Sidebar", "description": "Blender5専用: 自動UI展開・文字化け回避・設定完全書き出し版", "category": "308光円錐" } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0725_1800 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import time import re import os import webbrowser import addon_utils from mathutils import Vector, Euler # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "zionad 308 光円錐" PREFIX_VAL = "TCS_ARR_G5" ADDON_CATEGORY = "[ 308 光円錐 ]" PREFIX = PREFIX_VAL.lower() # ========================================================= # 5. リンク定"> Sidebar", "description": "Blender5専用: 自動UI展開・文字化け回避・設定完全書き出し版", "category": "308光円錐" } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0725_1800 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import time import re import os import webbrowser import addon_utils from mathutils import Vector, Euler # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "zionad 308 光円錐" PREFIX_VAL = "TCS_ARR_G5" ADDON_CATEGORY = "[ 308 光円錐 ]" PREFIX = PREFIX_VAL.lower() # ========================================================= # 5. リンク定"> Sidebar", "description": "Blender5専用: 自動UI展開・文字化け回避・設定完全書き出し版", "category": "308光円錐" } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0725_1800 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import time import re import os import webbrowser import addon_utils from mathutils import Vector, Euler # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "zionad 308 光円錐" PREFIX_VAL = "TCS_ARR_G5" ADDON_CATEGORY = "[ 308 光円錐 ]" PREFIX = PREFIX_VAL.lower() # ========================================================= # 5. リンク定">








bl_info = {
    "name": "zionad 308 光円錐",
    "author": "zionadchat",
    "version": (3, 2, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar",
    "description": "Blender5専用: 自動UI展開・文字化け回避・設定完全書き出し版",
    "category": "308光円錐"
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1800

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import re
import os
import webbrowser
import addon_utils
from mathutils import Vector, Euler

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "zionad 308 光円錐"
PREFIX_VAL = "TCS_ARR_G5"
ADDON_CATEGORY = "[ 308 光円錐 ]"
PREFIX = PREFIX_VAL.lower()

# =========================================================
# 5. リンク定義
# =========================================================
LNK_NM_1 = "308 トーラス等間隔 矢印"
LNK_UR_1 = "<https://app.notion.com/p/308-3a8347b3454a8055b265fd41eea1dcea>"
LNK_NM_2 = "コンテナ builder"
LNK_UR_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LNK_NM_3 = "posfie"
LNK_UR_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LNK_NM_4 = "zionadchat"
LNK_UR_4 = "<https://x.com/zionadchat>"

# =========================================================
# 6. 初期設定
# =========================================================
# --- DEFAULTS_BLOCK_START ---

# --- DEFAULTS_BLOCK_START ---

# 数値 (半径・高さ・太さ・本数など)
TR_MJ_RD_DFLT = 3.0
TR_MN_RD_DFLT = 0.20000000298023224
CN_RD_TP_DFLT = 0.0
CN_RD_BT_DFLT = 2.0
CN_HT_DFLT = 4.0
SP_RD_DFLT = 1.5
AW_CNT_DFLT = 8
AW_THK_DFLT = 0.10000000149011612
DM_RD_DFLT = 3.0
DM_THK_DFLT = 0.10000000149011612

# ドームの形状 ( 'HEMI': 半球, 'FULL': 球体 )
DM_MODE_DFLT = 'HEMI'

# XYZ (各オブジェクトの初期位置)
TR_LOC_X_DFLT = 0.0000
TR_LOC_Y_DFLT = 0.0000
TR_LOC_Z_DFLT = 0.0000

CN_LOC_X_DFLT = 5.0000
CN_LOC_Y_DFLT = 0.0000
CN_LOC_Z_DFLT = 0.0000

SP_LOC_X_DFLT = -5.0000
SP_LOC_Y_DFLT = 0.0000
SP_LOC_Z_DFLT = 0.0000

DM_LOC_X_DFLT = 0.0000
DM_LOC_Y_DFLT = 0.0000
DM_LOC_Z_DFLT = 0.0000

# 透明度
ALPHA_FRONT_DFLT = 1.0000
ALPHA_BACK_DFLT = 1.0000

# テキスト設定
TEXT_BODY_DFLT = "Sample Text"
TEXT_SIZE_DFLT = 1.5
TEXT_OFFSET_X_DFLT = 0.0000
TEXT_OFFSET_Y_DFLT = 0.0000
TEXT_OFFSET_Z_DFLT = 3.0000

# 色 (R, G, B, A)
COLOR_TR_DFLT = (0.2000, 0.6000, 1.0000, 1.0000)
COLOR_CN_FR_DFLT = (1.0000, 0.5000, 0.1000, 1.0000)
COLOR_CN_BK_DFLT = (0.1000, 0.5000, 1.0000, 1.0000)
COLOR_SP_DFLT = (0.2000, 0.9000, 0.4000, 1.0000)
COLOR_AW_DFLT = (1.0000, 0.1000, 0.1000, 1.0000)
COLOR_DM_DFLT = (0.5000, 0.3000, 0.8000, 1.0000)
COLOR_TX_DFLT = (1.0000, 1.0000, 1.0000, 1.0000)

# --- DEFAULTS_BLOCK_END ---

# =========================================================
# 7. コアユーティリティ
# =========================================================
def gt_cr_clc(nm_st):
    c_dt = bpy.data.collections.get(nm_st)
    if not c_dt:
        c_dt = bpy.data.collections.new(nm_st)
        bpy.context.scene.collection.children.link(c_dt)
    return c_dt

def gt_cr_mt(nm_st, cl_tp):
    m_dt = bpy.data.materials.get(nm_st)
    if not m_dt:
        m_dt = bpy.data.materials.new(nm_st)
        m_dt.use_nodes = True
        try:
            m_dt.blend_method = 'BLEND'
            m_dt.shadow_method = 'NONE'
        except Exception: pass
    if m_dt.use_nodes:
        bs_nd = m_dt.node_tree.nodes.get("Principled BSDF")
        if bs_nd:
            if "Base Color" in bs_nd.inputs: bs_nd.inputs["Base Color"].default_value = (cl_tp[0], cl_tp[1], cl_tp[2], 1.0)
            if "Alpha" in bs_nd.inputs: bs_nd.inputs["Alpha"].default_value = cl_tp[3]
    m_dt.diffuse_color = cl_tp
    return m_dt

def gt_cr_mt_dbl(nm_st, c_fr, c_bk):
    m_dt = bpy.data.materials.get(nm_st)
    if not m_dt:
        m_dt = bpy.data.materials.new(nm_st)
        m_dt.use_nodes = True
        try:
            m_dt.blend_method = 'BLEND'
            m_dt.shadow_method = 'NONE'
        except Exception: pass
        
    if m_dt.use_nodes:
        tr_dt = m_dt.node_tree
        bs_fr = tr_dt.nodes.get("Pr_Fr")
        if not bs_fr:
            tr_dt.nodes.clear()
            out_nd = tr_dt.nodes.new('ShaderNodeOutputMaterial')
            out_nd.location = (400, 0)
            mx_nd = tr_dt.nodes.new('ShaderNodeMixShader')
            mx_nd.location = (200, 0)
            bs_fr = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
            bs_fr.name = "Pr_Fr"
            bs_fr.location = (0, 150)
            bs_bk = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
            bs_bk.name = "Pr_Bk"
            bs_bk.location = (0, -200)
            gm_nd = tr_dt.nodes.new('ShaderNodeNewGeometry')
            gm_nd.location = (0, 400)
            
            tr_dt.links.new(gm_nd.outputs["Backfacing"], mx_nd.inputs[0])
            tr_dt.links.new(bs_fr.outputs["BSDF"], mx_nd.inputs[1])
            tr_dt.links.new(bs_bk.outputs["BSDF"], mx_nd.inputs[2])
            tr_dt.links.new(mx_nd.outputs["Shader"], out_nd.inputs["Surface"])
            
        bs_fr = tr_dt.nodes.get("Pr_Fr")
        bs_bk = tr_dt.nodes.get("Pr_Bk")
        
        if bs_fr:
            if "Base Color" in bs_fr.inputs: bs_fr.inputs["Base Color"].default_value = (c_fr[0], c_fr[1], c_fr[2], 1.0)
            if "Alpha" in bs_fr.inputs: bs_fr.inputs["Alpha"].default_value = c_fr[3]
        if bs_bk:
            if "Base Color" in bs_bk.inputs: bs_bk.inputs["Base Color"].default_value = (c_bk[0], c_bk[1], c_bk[2], 1.0)
            if "Alpha" in bs_bk.inputs: bs_bk.inputs["Alpha"].default_value = c_bk[3]
            
    m_dt.diffuse_color = c_fr
    return m_dt

def aply_dt(ob_nm, dt_fn, m_dt, dt_tp='MESH'):
    c_dt = gt_cr_clc(f"{PREFIX_VAL}_Col")
    ob_dt = bpy.data.objects.get(ob_nm)
    if ob_dt:
        od_dt = ob_dt.data
        ob_dt.data = dt_fn()
        if od_dt.users == 0:
            if dt_tp == 'MESH': bpy.data.meshes.remove(od_dt, do_unlink=True)
            elif dt_tp == 'CURVE': bpy.data.curves.remove(od_dt, do_unlink=True)
    else:
        nd_dt = dt_fn()
        ob_dt = bpy.data.objects.new(ob_nm, nd_dt)
        c_dt.objects.link(ob_dt)
        
    if not ob_dt.data.materials: ob_dt.data.materials.append(m_dt)
    else: ob_dt.data.materials[0] = m_dt
    return ob_dt

def dtch_ob(ctx, ob_nm, nw_nm, flg_st):
    ob_dt = bpy.data.objects.get(ob_nm)
    if not ob_dt: return False
    ts = time.strftime("%H%M%S")
    rn_nm = f"{nw_nm}_{ts}"
    ob_dt.name = rn_nm
    if ob_dt.data: ob_dt.data.name = f"{rn_nm}_Dt"
    for sl in ob_dt.material_slots:
        if sl.material and sl.material.name.startswith(PREFIX_VAL):
            nw_mt = sl.material.copy()
            nw_mt.name = f"{rn_nm}_Mt"
            sl.material = nw_mt
    m_cl = ctx.scene.collection
    if ob_dt.name not in m_cl.objects: m_cl.objects.link(ob_dt)
    a_cl = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
    if a_cl and ob_dt.name in a_cl.objects: a_cl.objects.unlink(ob_dt)
    setattr(ctx.scene.tc_g5_prp, flg_st, False)
    return True

# =========================================================
# 8. 更新処理 (Update)
# =========================================================
def upd_tr(sf, cx):
    if not sf.b_tr: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tr", sf.cl_tr)
    def mk_tr():
        v, f = [], []
        S, MS = 48, 16
        for i in range(S):
            a1 = (i/S)*2*math.pi
            c1, s1 = math.cos(a1), math.sin(a1)
            for j in range(MS):
                a2 = (j/MS)*2*math.pi
                c2, s2 = math.cos(a2), math.sin(a2)
                v.append(((sf.v_tr_mj + sf.v_tr_mn*c2)*c1, (sf.v_tr_mj + sf.v_tr_mn*c2)*s1, sf.v_tr_mn*s2))
        for i in range(S):
            for j in range(MS):
                i_n, j_n = (i+1)%S, (j+1)%MS
                f.append((i*MS+j, i_n*MS+j, i_n*MS+j_n, i*MS+j_n))
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Tr")
        me.from_pydata(v, [], f)
        me.update()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Tr", mk_tr, m_dt)
    ob.location, ob.rotation_euler = sf.lc_tr, sf.rt_tr
    if sf.b_aw: upd_aw(sf, cx)

def upd_cn(sf, cx):
    if not sf.b_cn: return
    m_dt = gt_cr_mt_dbl(f"{PREFIX_VAL}_Mt_Cn", sf.cl_cn_fr, sf.cl_cn_bk)
    def mk_cn():
        bm = bmesh.new()
        ah = max(abs(sf.v_cn_ht), 0.001)
        r1, r2, zo = (sf.v_cn_bt, sf.v_cn_tp, ah/2) if sf.v_cn_ht >= 0 else (sf.v_cn_tp, sf.v_cn_bt, -ah/2)
        bmesh.ops.create_cone(bm, cap_ends=not sf.b_cn_cp, cap_tris=True, segments=48, radius1=r1, radius2=r2, depth=ah)
        bmesh.ops.translate(bm, verts=bm.verts, vec=(0,0,zo))
        if sf.b_cn_fl: bmesh.ops.reverse_faces(bm, faces=bm.faces)
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Cn")
        bm.to_mesh(me)
        bm.free()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Cn", mk_cn, m_dt)
    ob.location, ob.rotation_euler = sf.lc_cn, sf.rt_cn

def upd_sp(sf, cx):
    if not sf.b_sp: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Sp", sf.cl_sp)
    def mk_sp():
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=sf.v_sp_rd)
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Sp")
        bm.to_mesh(me)
        bm.free()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Sp", mk_sp, m_dt)
    ob.location, ob.rotation_euler = sf.lc_sp, sf.rt_sp

def upd_aw(sf, cx):
    if not sf.b_aw: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Aw", sf.cl_aw)
    def mk_aw():
        bm = bmesh.new()
        tl, tm = Vector(sf.lc_tr), Euler(sf.rt_tr).to_matrix()
        for i in range(sf.v_aw_cn):
            th = i*(2*math.pi/sf.v_aw_cn)
            sp = tl + tm @ Vector((sf.v_tr_mj*math.cos(th), sf.v_tr_mj*math.sin(th), 0))
            vc = Vector(sf.lc_aw_tg) - sp
            ds = vc.length * (sf.v_aw_of/100.0)
            if ds < 0.001: continue
            hl = min(sf.v_aw_th*4.0, ds*0.5)
            sl = ds - hl
            rq = Vector((0,0,1)).rotation_difference(vc.normalized())
            gs = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th, radius2=sf.v_aw_th, depth=sl)
            bmesh.ops.translate(bm, verts=gs['verts'], vec=(0,0,sl/2))
            gh = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th*2.5, radius2=0, depth=hl)
            bmesh.ops.translate(bm, verts=gh['verts'], vec=(0,0,sl+hl/2))
            va = gs['verts'] + gh['verts']
            bmesh.ops.transform(bm, verts=va, matrix=rq.to_matrix().to_4x4())
            bmesh.ops.translate(bm, verts=va, vec=sp)
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Aw")
        bm.to_mesh(me)
        bm.free()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Aw", mk_aw, m_dt)
    ob.location, ob.rotation_euler = (0,0,0), (0,0,0)

def upd_dm(sf, cx):
    if not sf.b_dm: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Dm", sf.cl_dm)
    def mk_dm():
        cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Dm", 'CURVE')
        cv.dimensions, cv.fill_mode = '3D', 'FULL'
        cv.bevel_depth, cv.bevel_resolution = sf.v_dm_th, 4
        
        is_fl = (sf.s_dm_md == 'FULL')
        ph_mx = math.pi if is_fl else (math.pi / 2)
        lt_ed = (sf.v_dm_lt * 2) if is_fl else (sf.v_dm_lt + 1)
        
        for i in range(sf.v_dm_ln):
            th = (2*math.pi/sf.v_dm_ln)*i
            sl = cv.splines.new('POLY')
            sl.points.add(sf.v_dm_sg - 1)
            for j in range(sf.v_dm_sg):
                ph = ph_mx * (j/(sf.v_dm_sg - 1))
                sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th), 
                                   sf.v_dm_rd*math.sin(ph)*math.sin(th), 
                                   sf.v_dm_rd*math.cos(ph), 1)
        for i in range(1, lt_ed):
            ph = (math.pi/2)*(i/sf.v_dm_lt)
            sl = cv.splines.new('POLY')
            sl.points.add(sf.v_dm_sg - 1)
            for j in range(sf.v_dm_sg):
                th = (2*math.pi)*(j/(sf.v_dm_sg - 1))
                sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th), 
                                   sf.v_dm_rd*math.sin(ph)*math.sin(th), 
                                   sf.v_dm_rd*math.cos(ph), 1)
            sl.use_cyclic_u = True
        return cv
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Dm", mk_dm, m_dt, 'CURVE')
    ob.location, ob.rotation_euler = sf.lc_dm, sf.rt_dm

def upd_tx(sf, cx):
    if not sf.b_tx: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tx", sf.cl_tx)
    def mk_tx():
        cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Tx", 'FONT')
        cv.body = sf.s_tx_bd
        cv.size = sf.v_tx_sz
        cv.extrude = 0.05
        return cv
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Tx", mk_tx, m_dt, 'CURVE')
    ob.location = sf.lc_tx
    ob.rotation_euler = sf.rt_tx

# =========================================================
# 9. プロパティ定義
# =========================================================
class TC_G5_Prp(bpy.types.PropertyGroup):
    b_tr: bpy.props.BoolProperty(default=False)
    lc_tr: bpy.props.FloatVectorProperty(name="Location", default=(TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT), update=upd_tr)
    rt_tr: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_tr)
    v_tr_mj: bpy.props.FloatProperty(name="Major Radius", default=TR_MJ_RD_DFLT, update=upd_tr)
    v_tr_mn: bpy.props.FloatProperty(name="Minor Radius", default=TR_MN_RD_DFLT, update=upd_tr)
    cl_tr: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TR_DFLT, min=0, max=1, update=upd_tr)
    
    b_cn: bpy.props.BoolProperty(default=False)
    lc_cn: bpy.props.FloatVectorProperty(name="Location", default=(CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT), update=upd_cn)
    rt_cn: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_cn)
    v_cn_tp: bpy.props.FloatProperty(name="Top Radius", default=CN_RD_TP_DFLT, update=upd_cn)
    v_cn_bt: bpy.props.FloatProperty(name="Bot Radius", default=CN_RD_BT_DFLT, update=upd_cn)
    v_cn_ht: bpy.props.FloatProperty(name="Height", default=CN_HT_DFLT, update=upd_cn)
    b_cn_cp: bpy.props.BoolProperty(name="No Caps", default=False, update=upd_cn)
    b_cn_fl: bpy.props.BoolProperty(name="Flip Norm", default=False, update=upd_cn)
    cl_cn_fr: bpy.props.FloatVectorProperty(name="Front Col", subtype='COLOR', size=4, default=COLOR_CN_FR_DFLT, min=0, max=1, update=upd_cn)
    cl_cn_bk: bpy.props.FloatVectorProperty(name="Back Col", subtype='COLOR', size=4, default=COLOR_CN_BK_DFLT, min=0, max=1, update=upd_cn)

    b_sp: bpy.props.BoolProperty(default=False)
    lc_sp: bpy.props.FloatVectorProperty(name="Location", default=(SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT), update=upd_sp)
    rt_sp: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_sp)
    v_sp_rd: bpy.props.FloatProperty(name="Radius", default=SP_RD_DFLT, update=upd_sp)
    cl_sp: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_SP_DFLT, min=0, max=1, update=upd_sp)

    b_aw: bpy.props.BoolProperty(default=False)
    v_aw_cn: bpy.props.IntProperty(name="Count", default=AW_CNT_DFLT, min=1, max=100, update=upd_aw)
    lc_aw_tg: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10), update=upd_aw)
    v_aw_of: bpy.props.FloatProperty(name="Offset %", default=100.0, min=1, max=100, update=upd_aw)
    v_aw_th: bpy.props.FloatProperty(name="Thickness", default=AW_THK_DFLT, update=upd_aw)
    cl_aw: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_AW_DFLT, min=0, max=1, update=upd_aw)

    b_dm: bpy.props.BoolProperty(default=False)
    s_dm_md: bpy.props.EnumProperty(
        items=[('HEMI', "半球 (Dome)", ""), ('FULL', "球体 (Sphere)", "")], 
        default=DM_MODE_DFLT, update=upd_dm
    )
    lc_dm: bpy.props.FloatVectorProperty(name="Location", default=(DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT), update=upd_dm)
    rt_dm: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_dm)
    v_dm_rd: bpy.props.FloatProperty(name="Radius", default=DM_RD_DFLT, update=upd_dm)
    v_dm_th: bpy.props.FloatProperty(name="Thickness", default=DM_THK_DFLT, update=upd_dm)
    v_dm_ln: bpy.props.IntProperty(name="Longt Count", default=12, update=upd_dm)
    v_dm_lt: bpy.props.IntProperty(name="Lat Count (per Hemi)", default=6, update=upd_dm)
    v_dm_sg: bpy.props.IntProperty(name="Segments", default=32, update=upd_dm)
    cl_dm: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_DM_DFLT, min=0, max=1, update=upd_dm)

    b_tx: bpy.props.BoolProperty(default=False)
    s_tx_bd: bpy.props.StringProperty(name="Text", default=TEXT_BODY_DFLT, update=upd_tx)
    v_tx_sz: bpy.props.FloatProperty(name="Size", default=TEXT_SIZE_DFLT, update=upd_tx)
    lc_tx: bpy.props.FloatVectorProperty(name="Location", default=(TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT), update=upd_tx)
    rt_tx: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(1.5708,0,0), update=upd_tx)
    cl_tx: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TX_DFLT, min=0, max=1, update=upd_tx)

# =========================================================
# 10. オペレーター群
# =========================================================
class Op_Cr_Tr(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_tr"
    bl_label = "トーラス生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_tr = True
        upd_tr(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Cn(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_cn"
    bl_label = "円錐台生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_cn = True
        upd_cn(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Sp(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_sp"
    bl_label = "球体生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_sp = True
        upd_sp(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Aw(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_aw"
    bl_label = "集中矢印生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_aw = True
        upd_aw(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Dm(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_dm"
    bl_label = "骨格生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_dm = True
        upd_dm(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}
        
class Op_Cr_Tx(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_tx"
    bl_label = "テキスト生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_tx = True
        upd_tx(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Dt_Tr(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_tr"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tr", "Detached_Torus", "b_tr")
        return {'FINISHED'}

class Op_Dt_Cn(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_cn"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Cn", "Detached_Cone", "b_cn")
        return {'FINISHED'}

class Op_Dt_Sp(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_sp"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Sp", "Detached_Sphere", "b_sp")
        return {'FINISHED'}

class Op_Dt_Aw(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_aw"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Aw", "Detached_Arrows", "b_aw")
        return {'FINISHED'}

class Op_Dt_Dm(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_dm"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Dm", "Detached_Bone", "b_dm")
        return {'FINISHED'}
        
class Op_Dt_Tx(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_tx"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tx", "Detached_Text", "b_tx")
        return {'FINISHED'}

class Op_Op_Ur(bpy.types.Operator):
    bl_idname = f"{PREFIX}.op_ur"
    bl_label = "Open URL"
    url: bpy.props.StringProperty()
    def execute(self, cx):
        webbrowser.open(self.url)
        return {'FINISHED'}

def dly_unrg():
    try: addon_utils.disable(__name__)
    except Exception: pass
    try: unregister()
    except Exception: pass
    return None

class Op_Rm_Ad(bpy.types.Operator):
    bl_idname = f"{PREFIX}.rm_ad"
    bl_label = "Clean up & Uninstall"
    def execute(self, cx):
        c_dt = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
        if c_dt:
            for ob in list(c_dt.objects): bpy.data.objects.remove(ob, do_unlink=True)
            bpy.data.collections.remove(c_dt)
        for m in list(bpy.data.meshes):
            if m.name.startswith(PREFIX_VAL): bpy.data.meshes.remove(m, do_unlink=True)
        for c in list(bpy.data.curves):
            if c.name.startswith(PREFIX_VAL): bpy.data.curves.remove(c, do_unlink=True)
        for mt in list(bpy.data.materials):
            if mt.name.startswith(PREFIX_VAL): bpy.data.materials.remove(mt, do_unlink=True)
        bpy.app.timers.register(dly_unrg, first_interval=0.2)
        return {'FINISHED'}

class Op_Xp_Tx(bpy.types.Operator):
    bl_idname = f"{PREFIX}.xp_tx"
    bl_label = "設定をコードに書き出し"
    def execute(self, cx):
        if not hasattr(cx.scene, "tc_g5_prp"): return {'CANCELLED'}
        p = cx.scene.tc_g5_prp
        ss = ""
        fp = globals().get("__file__")
        if fp and fp.endswith(".pyc"): fp = fp[:-1]
        if fp and os.path.exists(fp):
            try:
                with open(fp, "r", encoding="utf-8") as f: ss = f.read()
            except Exception: pass
        if not ss:
            for tx in bpy.data.texts:
                bd = tx.as_string()
                if ("# " + "--- DEFAULTS_BLOCK_START ---") in bd:
                    ss = bd
                    break
        if not ss: return {'CANCELLED'}
        
        # 初期設定ブロックと「完全に同じ形式」で出力文字列を構築
        ds = "# " + "--- DEFAULTS_BLOCK_START ---\n\n"
        
        ds += "# 数値 (半径・高さ・太さ・本数など)\n"
        ds += f"TR_MJ_RD_DFLT = {p.v_tr_mj}\n"
        ds += f"TR_MN_RD_DFLT = {p.v_tr_mn}\n"
        ds += f"CN_RD_TP_DFLT = {p.v_cn_tp}\n"
        ds += f"CN_RD_BT_DFLT = {p.v_cn_bt}\n"
        ds += f"CN_HT_DFLT = {p.v_cn_ht}\n"
        ds += f"SP_RD_DFLT = {p.v_sp_rd}\n"
        ds += f"AW_CNT_DFLT = {p.v_aw_cn}\n"
        ds += f"AW_THK_DFLT = {p.v_aw_th}\n"
        ds += f"DM_RD_DFLT = {p.v_dm_rd}\n"
        ds += f"DM_THK_DFLT = {p.v_dm_th}\n\n"
        
        ds += "# ドームの形状 ( 'HEMI': 半球, 'FULL': 球体 )\n"
        ds += f"DM_MODE_DFLT = '{p.s_dm_md}'\n\n"
        
        ds += "# XYZ (各オブジェクトの初期位置)\n"
        ds += f"TR_LOC_X_DFLT = {p.lc_tr[0]:.4f}\n"
        ds += f"TR_LOC_Y_DFLT = {p.lc_tr[1]:.4f}\n"
        ds += f"TR_LOC_Z_DFLT = {p.lc_tr[2]:.4f}\n\n"
        
        ds += f"CN_LOC_X_DFLT = {p.lc_cn[0]:.4f}\n"
        ds += f"CN_LOC_Y_DFLT = {p.lc_cn[1]:.4f}\n"
        ds += f"CN_LOC_Z_DFLT = {p.lc_cn[2]:.4f}\n\n"
        
        ds += f"SP_LOC_X_DFLT = {p.lc_sp[0]:.4f}\n"
        ds += f"SP_LOC_Y_DFLT = {p.lc_sp[1]:.4f}\n"
        ds += f"SP_LOC_Z_DFLT = {p.lc_sp[2]:.4f}\n\n"
        
        ds += f"DM_LOC_X_DFLT = {p.lc_dm[0]:.4f}\n"
        ds += f"DM_LOC_Y_DFLT = {p.lc_dm[1]:.4f}\n"
        ds += f"DM_LOC_Z_DFLT = {p.lc_dm[2]:.4f}\n\n"
        
        ds += "# 透明度\n"
        ds += f"ALPHA_FRONT_DFLT = {p.cl_cn_fr[3]:.4f}\n"
        ds += f"ALPHA_BACK_DFLT = {p.cl_cn_bk[3]:.4f}\n\n"
        
        ds += "# テキスト設定\n"
        ds += f"TEXT_BODY_DFLT = \"{p.s_tx_bd}\"\n"
        ds += f"TEXT_SIZE_DFLT = {p.v_tx_sz}\n"
        ds += f"TEXT_OFFSET_X_DFLT = {p.lc_tx[0]:.4f}\n"
        ds += f"TEXT_OFFSET_Y_DFLT = {p.lc_tx[1]:.4f}\n"
        ds += f"TEXT_OFFSET_Z_DFLT = {p.lc_tx[2]:.4f}\n\n"
        
        ds += "# 色 (R, G, B, A)\n"
        ds += f"COLOR_TR_DFLT = ({p.cl_tr[0]:.4f}, {p.cl_tr[1]:.4f}, {p.cl_tr[2]:.4f}, {p.cl_tr[3]:.4f})\n"
        ds += f"COLOR_CN_FR_DFLT = ({p.cl_cn_fr[0]:.4f}, {p.cl_cn_fr[1]:.4f}, {p.cl_cn_fr[2]:.4f}, {p.cl_cn_fr[3]:.4f})\n"
        ds += f"COLOR_CN_BK_DFLT = ({p.cl_cn_bk[0]:.4f}, {p.cl_cn_bk[1]:.4f}, {p.cl_cn_bk[2]:.4f}, {p.cl_cn_bk[3]:.4f})\n"
        ds += f"COLOR_SP_DFLT = ({p.cl_sp[0]:.4f}, {p.cl_sp[1]:.4f}, {p.cl_sp[2]:.4f}, {p.cl_sp[3]:.4f})\n"
        ds += f"COLOR_AW_DFLT = ({p.cl_aw[0]:.4f}, {p.cl_aw[1]:.4f}, {p.cl_aw[2]:.4f}, {p.cl_aw[3]:.4f})\n"
        ds += f"COLOR_DM_DFLT = ({p.cl_dm[0]:.4f}, {p.cl_dm[1]:.4f}, {p.cl_dm[2]:.4f}, {p.cl_dm[3]:.4f})\n"
        ds += f"COLOR_TX_DFLT = ({p.cl_tx[0]:.4f}, {p.cl_tx[1]:.4f}, {p.cl_tx[2]:.4f}, {p.cl_tx[3]:.4f})\n\n"
        
        m1 = "# " + "--- DEFAULTS_BLOCK_START ---"
        m2 = "# " + "--- DEFAULTS_BLOCK_END ---"
        pr = r"(" + m1 + r"\n)(.*?)(" + m2 + r")"
        ns = re.sub(pr, r"\g<1>\n" + ds + r"\g<3>", ss, flags=re.DOTALL)
        
        tn = "Exp_TCS_G5.py"
        nb = bpy.data.texts.get(tn)
        if nb: nb.clear()
        else: nb = bpy.data.texts.new(tn)
        nb.write(ns)
        
        sf = False
        for a in cx.screen.areas:
            if a.type == 'TEXT_EDITOR':
                a.spaces.active.text = nb
                sf = True
                break
        if sf: self.report({'INFO'}, f"書き出し成功: {tn}")
        else: self.report({'INFO'}, f"内部データに保存: {tn}")
        return {'FINISHED'}

# =========================================================
# 11. UIパネル
# =========================================================
class Pn_Bs(bpy.types.Panel):
    bl_space_type, bl_region_type, bl_category = 'VIEW_3D', 'UI', ADDON_CATEGORY

class Pn_Tr(Pn_Bs):
    bl_label, bl_idname, bl_order = "1. トーラス", f"{PREFIX_VAL}_P_Tr", 1
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_tr"); c.prop(p, "rt_tr"); c.separator()
        c.prop(p, "v_tr_mj"); c.prop(p, "v_tr_mn"); c.prop(p, "cl_tr"); ly.separator()
        r = ly.row(align=True)
        if not p.b_tr: r.operator(Op_Cr_Tr.bl_idname, icon='MESH_TORUS')
        else:
            r.operator(Op_Cr_Tr.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Tr.bl_idname, icon='UNLINKED')

class Pn_Cn(Pn_Bs):
    bl_label, bl_idname, bl_order = "2. 円錐台", f"{PREFIX_VAL}_P_Cn", 2
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_cn"); c.prop(p, "rt_cn"); c.separator()
        r1 = c.row(align=True); r1.prop(p, "v_cn_tp"); r1.prop(p, "v_cn_bt")
        c.prop(p, "v_cn_ht"); c.separator()
        r2 = c.row(align=True); r2.prop(p, "b_cn_cp"); r2.prop(p, "b_cn_fl"); c.separator()
        r3 = c.row(align=True); r3.prop(p, "cl_cn_fr", text="表面"); r3.prop(p, "cl_cn_bk", text="裏面"); ly.separator()
        r = ly.row(align=True)
        if not p.b_cn: r.operator(Op_Cr_Cn.bl_idname, icon='MESH_CONE')
        else:
            r.operator(Op_Cr_Cn.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Cn.bl_idname, icon='UNLINKED')

class Pn_Sp(Pn_Bs):
    bl_label, bl_idname, bl_order = "3. 球体", f"{PREFIX_VAL}_P_Sp", 3
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_sp"); c.prop(p, "rt_sp"); c.separator()
        c.prop(p, "v_sp_rd"); c.prop(p, "cl_sp"); ly.separator()
        r = ly.row(align=True)
        if not p.b_sp: r.operator(Op_Cr_Sp.bl_idname, icon='MESH_UVSPHERE')
        else:
            r.operator(Op_Cr_Sp.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Sp.bl_idname, icon='UNLINKED')

class Pn_Aw(Pn_Bs):
    bl_label, bl_idname, bl_order = "4. 集中矢印", f"{PREFIX_VAL}_P_Aw", 4
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "v_aw_cn"); c.prop(p, "lc_aw_tg"); c.prop(p, "v_aw_of"); c.separator()
        c.prop(p, "v_aw_th"); c.prop(p, "cl_aw"); ly.separator()
        r = ly.row(align=True)
        if not p.b_aw: r.operator(Op_Cr_Aw.bl_idname, icon='TRACKING')
        else:
            r.operator(Op_Cr_Aw.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Aw.bl_idname, icon='UNLINKED')

class Pn_Dm(Pn_Bs):
    bl_label, bl_idname, bl_order = "5. 骨格形状", f"{PREFIX_VAL}_P_Dm", 5
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "s_dm_md", text="形状")
        c.separator()
        c.prop(p, "lc_dm"); c.prop(p, "rt_dm"); c.separator()
        c.prop(p, "v_dm_rd"); c.prop(p, "v_dm_th"); c.separator()
        r1 = c.row(align=True); r1.prop(p, "v_dm_ln"); r1.prop(p, "v_dm_lt")
        c.prop(p, "v_dm_sg"); c.separator(); c.prop(p, "cl_dm"); ly.separator()
        r = ly.row(align=True)
        if not p.b_dm: r.operator(Op_Cr_Dm.bl_idname, icon='MESH_UVSPHERE')
        else:
            r.operator(Op_Cr_Dm.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Dm.bl_idname, icon='UNLINKED')

class Pn_Tx(Pn_Bs):
    bl_label, bl_idname, bl_order = "6. 3Dテキスト", f"{PREFIX_VAL}_P_Tx", 6
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "s_tx_bd"); c.prop(p, "v_tx_sz"); c.separator()
        c.prop(p, "lc_tx"); c.prop(p, "rt_tx"); c.separator(); c.prop(p, "cl_tx"); ly.separator()
        r = ly.row(align=True)
        if not p.b_tx: r.operator(Op_Cr_Tx.bl_idname, icon='FONT_DATA')
        else:
            r.operator(Op_Cr_Tx.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Tx.bl_idname, icon='UNLINKED')

class Pn_Lnk(Pn_Bs):
    bl_label, bl_idname, bl_order = "Links", f"{PREFIX_VAL}_P_Lnk", 10
    def draw(self, cx):
        b = self.layout.box()
        if LNK_NM_1: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_1, icon='URL').url = LNK_UR_1
        if LNK_NM_2: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_2, icon='URL').url = LNK_UR_2
        if LNK_NM_3: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_3, icon='URL').url = LNK_UR_3
        if LNK_NM_4: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_4, icon='URL').url = LNK_UR_4

class Pn_Rm(Pn_Bs):
    bl_label, bl_idname, bl_order, bl_options = "管理 & エクスポート", f"{PREFIX_VAL}_P_Rm", 11, {'DEFAULT_CLOSED'}
    def draw(self, cx):
        bx = self.layout.box()
        bx.operator(Op_Xp_Tx.bl_idname, icon='TEXT')
        bx.separator()
        bx.operator(Op_Rm_Ad.bl_idname, icon='CANCEL')

# =========================================================
# 12. 起動時自動展開処理
# =========================================================
def ui_st_up():
    try:
        cx = bpy.context
        if not cx or not cx.window_manager.windows: return 0.2
        for w in cx.window_manager.windows:
            for a in w.screen.areas:
                if a.type == 'VIEW_3D':
                    for s in a.spaces:
                        if s.type == 'VIEW_3D':
                            s.shading.type = 'MATERIAL'
                            s.show_region_ui = True
    except Exception: pass
    return None

cls_ls = [
    TC_G5_Prp, Op_Cr_Tr, Op_Cr_Cn, Op_Cr_Sp, Op_Cr_Aw, Op_Cr_Dm, Op_Cr_Tx, Op_Op_Ur,
    Op_Dt_Tr, Op_Dt_Cn, Op_Dt_Sp, Op_Dt_Aw, Op_Dt_Dm, Op_Dt_Tx,
    Op_Rm_Ad, Op_Xp_Tx,
    Pn_Tr, Pn_Cn, Pn_Sp, Pn_Aw, Pn_Dm, Pn_Tx, Pn_Lnk, Pn_Rm
]

def register():
    for c in cls_ls: bpy.utils.register_class(c)
    bpy.types.Scene.tc_g5_prp = bpy.props.PointerProperty(type=TC_G5_Prp)
    if not bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.register(ui_st_up, first_interval=0.2)

def unregister():
    try: del bpy.types.Scene.tc_g5_prp
    except Exception: pass
    for c in reversed(cls_ls):
        try: bpy.utils.unregister_class(c)
        except RuntimeError: pass
    if bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.unregister(ui_st_up)

if __name__ == "__main__": register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
    "name": "zionad 308 光円錐",
    "author": "zionadchat",
    "version": (3, 1, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar",
    "description": "Blender5専用: 自動UI展開・文字化け回避・半球/全球切替対応版",
    "category": "308光円錐"
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1730

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import re
import os
import webbrowser
import addon_utils
from mathutils import Vector, Euler

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "zionad 308 光円錐"
PREFIX_VAL = "TCS_ARR_G5"
ADDON_CATEGORY = "308光円錐"
PREFIX = PREFIX_VAL.lower()

# =========================================================
# 5. リンク定義
# =========================================================
LNK_NM_1 = "308 トーラス等間隔 矢印"
LNK_UR_1 = "<https://app.notion.com/p/308-3a8347b3454a8055b265fd41eea1dcea>"
LNK_NM_2 = "コンテナ builder"
LNK_UR_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LNK_NM_3 = "posfie"
LNK_UR_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LNK_NM_4 = "zionadchat"
LNK_UR_4 = "<https://x.com/zionadchat>"

# =========================================================
# 6. 初期設定
# =========================================================
# --- DEFAULTS_BLOCK_START ---

# 数値 (半径・高さ・太さ・本数など)
TR_MJ_RD_DFLT = 3.0
TR_MN_RD_DFLT = 0.2
CN_RD_TP_DFLT = 0.0
CN_RD_BT_DFLT = 2.0
CN_HT_DFLT = 4.0
SP_RD_DFLT = 1.5
AW_CNT_DFLT = 8
AW_THK_DFLT = 0.1
DM_RD_DFLT = 3.0
DM_THK_DFLT = 0.1

# ドームの形状 ( 'HEMI': 半球, 'FULL': 球体 )
DM_MODE_DFLT = 'HEMI'

# XYZ (各オブジェクトの初期位置)
TR_LOC_X_DFLT = 0.0
TR_LOC_Y_DFLT = 0.0
TR_LOC_Z_DFLT = 0.0

CN_LOC_X_DFLT = 5.0
CN_LOC_Y_DFLT = 0.0
CN_LOC_Z_DFLT = 0.0

SP_LOC_X_DFLT = -5.0
SP_LOC_Y_DFLT = 0.0
SP_LOC_Z_DFLT = 0.0

DM_LOC_X_DFLT = 0.0
DM_LOC_Y_DFLT = 5.0
DM_LOC_Z_DFLT = 0.0

# 透明度
ALPHA_FRONT_DFLT = 1.0
ALPHA_BACK_DFLT = 1.0

# テキスト設定
TEXT_BODY_DFLT = "Sample Text"
TEXT_SIZE_DFLT = 1.5
TEXT_OFFSET_X_DFLT = 0.0
TEXT_OFFSET_Y_DFLT = 0.0
TEXT_OFFSET_Z_DFLT = 3.0

# 色 (R, G, B, A)
COLOR_TR_DFLT = (0.2000, 0.6000, 1.0000, 1.0000)
COLOR_CN_FR_DFLT = (1.0000, 0.5000, 0.1000, 1.0000)
COLOR_CN_BK_DFLT = (0.1000, 0.5000, 1.0000, 1.0000)
COLOR_SP_DFLT = (0.2000, 0.9000, 0.4000, 1.0000)
COLOR_AW_DFLT = (1.0000, 0.1000, 0.1000, 1.0000)
COLOR_DM_DFLT = (0.5000, 0.3000, 0.8000, 1.0000)
COLOR_TX_DFLT = (1.0000, 1.0000, 1.0000, 1.0000)

# --- DEFAULTS_BLOCK_END ---

# =========================================================
# 7. コアユーティリティ (文字化け回避対応済)
# =========================================================
def gt_cr_clc(nm_st):
    c_dt = bpy.data.collections.get(nm_st)
    if not c_dt:
        c_dt = bpy.data.collections.new(nm_st)
        bpy.context.scene.collection.children.link(c_dt)
    return c_dt

def gt_cr_mt(nm_st, cl_tp):
    m_dt = bpy.data.materials.get(nm_st)
    if not m_dt:
        m_dt = bpy.data.materials.new(nm_st)
        m_dt.use_nodes = True
        try:
            m_dt.blend_method = 'BLEND'
            m_dt.shadow_method = 'NONE'
        except Exception: pass
    if m_dt.use_nodes:
        bs_nd = m_dt.node_tree.nodes.get("Principled BSDF")
        if bs_nd:
            if "Base Color" in bs_nd.inputs: bs_nd.inputs["Base Color"].default_value = (cl_tp[0], cl_tp[1], cl_tp[2], 1.0)
            if "Alpha" in bs_nd.inputs: bs_nd.inputs["Alpha"].default_value = cl_tp[3]
    m_dt.diffuse_color = cl_tp
    return m_dt

def gt_cr_mt_dbl(nm_st, c_fr, c_bk):
    m_dt = bpy.data.materials.get(nm_st)
    if not m_dt:
        m_dt = bpy.data.materials.new(nm_st)
        m_dt.use_nodes = True
        try:
            m_dt.blend_method = 'BLEND'
            m_dt.shadow_method = 'NONE'
        except Exception: pass
        
    if m_dt.use_nodes:
        tr_dt = m_dt.node_tree
        bs_fr = tr_dt.nodes.get("Pr_Fr")
        if not bs_fr:
            tr_dt.nodes.clear()
            out_nd = tr_dt.nodes.new('ShaderNodeOutputMaterial')
            out_nd.location = (400, 0)
            mx_nd = tr_dt.nodes.new('ShaderNodeMixShader')
            mx_nd.location = (200, 0)
            bs_fr = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
            bs_fr.name = "Pr_Fr"
            bs_fr.location = (0, 150)
            bs_bk = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
            bs_bk.name = "Pr_Bk"
            bs_bk.location = (0, -200)
            gm_nd = tr_dt.nodes.new('ShaderNodeNewGeometry')
            gm_nd.location = (0, 400)
            
            tr_dt.links.new(gm_nd.outputs["Backfacing"], mx_nd.inputs[0])
            tr_dt.links.new(bs_fr.outputs["BSDF"], mx_nd.inputs[1])
            tr_dt.links.new(bs_bk.outputs["BSDF"], mx_nd.inputs[2])
            tr_dt.links.new(mx_nd.outputs["Shader"], out_nd.inputs["Surface"])
            
        bs_fr = tr_dt.nodes.get("Pr_Fr")
        bs_bk = tr_dt.nodes.get("Pr_Bk")
        
        if bs_fr:
            if "Base Color" in bs_fr.inputs: bs_fr.inputs["Base Color"].default_value = (c_fr[0], c_fr[1], c_fr[2], 1.0)
            if "Alpha" in bs_fr.inputs: bs_fr.inputs["Alpha"].default_value = c_fr[3]
        if bs_bk:
            if "Base Color" in bs_bk.inputs: bs_bk.inputs["Base Color"].default_value = (c_bk[0], c_bk[1], c_bk[2], 1.0)
            if "Alpha" in bs_bk.inputs: bs_bk.inputs["Alpha"].default_value = c_bk[3]
            
    m_dt.diffuse_color = c_fr
    return m_dt

def aply_dt(ob_nm, dt_fn, m_dt, dt_tp='MESH'):
    c_dt = gt_cr_clc(f"{PREFIX_VAL}_Col")
    ob_dt = bpy.data.objects.get(ob_nm)
    if ob_dt:
        od_dt = ob_dt.data
        ob_dt.data = dt_fn()
        if od_dt.users == 0:
            if dt_tp == 'MESH': bpy.data.meshes.remove(od_dt, do_unlink=True)
            elif dt_tp == 'CURVE': bpy.data.curves.remove(od_dt, do_unlink=True)
    else:
        nd_dt = dt_fn()
        ob_dt = bpy.data.objects.new(ob_nm, nd_dt)
        c_dt.objects.link(ob_dt)
        
    if not ob_dt.data.materials: ob_dt.data.materials.append(m_dt)
    else: ob_dt.data.materials[0] = m_dt
    return ob_dt

def dtch_ob(ctx, ob_nm, nw_nm, flg_st):
    ob_dt = bpy.data.objects.get(ob_nm)
    if not ob_dt: return False
    ts = time.strftime("%H%M%S")
    rn_nm = f"{nw_nm}_{ts}"
    ob_dt.name = rn_nm
    if ob_dt.data: ob_dt.data.name = f"{rn_nm}_Dt"
    for sl in ob_dt.material_slots:
        if sl.material and sl.material.name.startswith(PREFIX_VAL):
            nw_mt = sl.material.copy()
            nw_mt.name = f"{rn_nm}_Mt"
            sl.material = nw_mt
    m_cl = ctx.scene.collection
    if ob_dt.name not in m_cl.objects: m_cl.objects.link(ob_dt)
    a_cl = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
    if a_cl and ob_dt.name in a_cl.objects: a_cl.objects.unlink(ob_dt)
    setattr(ctx.scene.tc_g5_prp, flg_st, False)
    return True

# =========================================================
# 8. 更新処理 (Update)
# =========================================================
def upd_tr(sf, cx):
    if not sf.b_tr: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tr", sf.cl_tr)
    def mk_tr():
        v, f = [], []
        S, MS = 48, 16
        for i in range(S):
            a1 = (i/S)*2*math.pi
            c1, s1 = math.cos(a1), math.sin(a1)
            for j in range(MS):
                a2 = (j/MS)*2*math.pi
                c2, s2 = math.cos(a2), math.sin(a2)
                v.append(((sf.v_tr_mj + sf.v_tr_mn*c2)*c1, (sf.v_tr_mj + sf.v_tr_mn*c2)*s1, sf.v_tr_mn*s2))
        for i in range(S):
            for j in range(MS):
                i_n, j_n = (i+1)%S, (j+1)%MS
                f.append((i*MS+j, i_n*MS+j, i_n*MS+j_n, i*MS+j_n))
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Tr")
        me.from_pydata(v, [], f)
        me.update()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Tr", mk_tr, m_dt)
    ob.location, ob.rotation_euler = sf.lc_tr, sf.rt_tr
    if sf.b_aw: upd_aw(sf, cx)

def upd_cn(sf, cx):
    if not sf.b_cn: return
    m_dt = gt_cr_mt_dbl(f"{PREFIX_VAL}_Mt_Cn", sf.cl_cn_fr, sf.cl_cn_bk)
    def mk_cn():
        bm = bmesh.new()
        ah = max(abs(sf.v_cn_ht), 0.001)
        r1, r2, zo = (sf.v_cn_bt, sf.v_cn_tp, ah/2) if sf.v_cn_ht >= 0 else (sf.v_cn_tp, sf.v_cn_bt, -ah/2)
        bmesh.ops.create_cone(bm, cap_ends=not sf.b_cn_cp, cap_tris=True, segments=48, radius1=r1, radius2=r2, depth=ah)
        bmesh.ops.translate(bm, verts=bm.verts, vec=(0,0,zo))
        if sf.b_cn_fl: bmesh.ops.reverse_faces(bm, faces=bm.faces)
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Cn")
        bm.to_mesh(me)
        bm.free()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Cn", mk_cn, m_dt)
    ob.location, ob.rotation_euler = sf.lc_cn, sf.rt_cn

def upd_sp(sf, cx):
    if not sf.b_sp: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Sp", sf.cl_sp)
    def mk_sp():
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=sf.v_sp_rd)
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Sp")
        bm.to_mesh(me)
        bm.free()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Sp", mk_sp, m_dt)
    ob.location, ob.rotation_euler = sf.lc_sp, sf.rt_sp

def upd_aw(sf, cx):
    if not sf.b_aw: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Aw", sf.cl_aw)
    def mk_aw():
        bm = bmesh.new()
        tl, tm = Vector(sf.lc_tr), Euler(sf.rt_tr).to_matrix()
        for i in range(sf.v_aw_cn):
            th = i*(2*math.pi/sf.v_aw_cn)
            sp = tl + tm @ Vector((sf.v_tr_mj*math.cos(th), sf.v_tr_mj*math.sin(th), 0))
            vc = Vector(sf.lc_aw_tg) - sp
            ds = vc.length * (sf.v_aw_of/100.0)
            if ds < 0.001: continue
            hl = min(sf.v_aw_th*4.0, ds*0.5)
            sl = ds - hl
            rq = Vector((0,0,1)).rotation_difference(vc.normalized())
            gs = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th, radius2=sf.v_aw_th, depth=sl)
            bmesh.ops.translate(bm, verts=gs['verts'], vec=(0,0,sl/2))
            gh = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th*2.5, radius2=0, depth=hl)
            bmesh.ops.translate(bm, verts=gh['verts'], vec=(0,0,sl+hl/2))
            va = gs['verts'] + gh['verts']
            bmesh.ops.transform(bm, verts=va, matrix=rq.to_matrix().to_4x4())
            bmesh.ops.translate(bm, verts=va, vec=sp)
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Aw")
        bm.to_mesh(me)
        bm.free()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Aw", mk_aw, m_dt)
    ob.location, ob.rotation_euler = (0,0,0), (0,0,0)

def upd_dm(sf, cx):
    if not sf.b_dm: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Dm", sf.cl_dm)
    def mk_dm():
        cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Dm", 'CURVE')
        cv.dimensions, cv.fill_mode = '3D', 'FULL'
        cv.bevel_depth, cv.bevel_resolution = sf.v_dm_th, 4
        
        is_fl = (sf.s_dm_md == 'FULL')
        ph_mx = math.pi if is_fl else (math.pi / 2)
        lt_ed = (sf.v_dm_lt * 2) if is_fl else (sf.v_dm_lt + 1)
        
        # 経度 (縦線)
        for i in range(sf.v_dm_ln):
            th = (2*math.pi/sf.v_dm_ln)*i
            sl = cv.splines.new('POLY')
            sl.points.add(sf.v_dm_sg - 1)
            for j in range(sf.v_dm_sg):
                ph = ph_mx * (j/(sf.v_dm_sg - 1))
                sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th), 
                                   sf.v_dm_rd*math.sin(ph)*math.sin(th), 
                                   sf.v_dm_rd*math.cos(ph), 1)
        # 緯度 (横線)
        for i in range(1, lt_ed):
            ph = (math.pi/2)*(i/sf.v_dm_lt)
            sl = cv.splines.new('POLY')
            sl.points.add(sf.v_dm_sg - 1)
            for j in range(sf.v_dm_sg):
                th = (2*math.pi)*(j/(sf.v_dm_sg - 1))
                sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th), 
                                   sf.v_dm_rd*math.sin(ph)*math.sin(th), 
                                   sf.v_dm_rd*math.cos(ph), 1)
            sl.use_cyclic_u = True
        return cv
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Dm", mk_dm, m_dt, 'CURVE')
    ob.location, ob.rotation_euler = sf.lc_dm, sf.rt_dm

def upd_tx(sf, cx):
    if not sf.b_tx: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tx", sf.cl_tx)
    def mk_tx():
        cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Tx", 'FONT')
        cv.body = sf.s_tx_bd
        cv.size = sf.v_tx_sz
        cv.extrude = 0.05
        return cv
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Tx", mk_tx, m_dt, 'CURVE')
    ob.location = sf.lc_tx
    ob.rotation_euler = sf.rt_tx

# =========================================================
# 9. プロパティ定義
# =========================================================
class TC_G5_Prp(bpy.types.PropertyGroup):
    b_tr: bpy.props.BoolProperty(default=False)
    lc_tr: bpy.props.FloatVectorProperty(name="Location", default=(TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT), update=upd_tr)
    rt_tr: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_tr)
    v_tr_mj: bpy.props.FloatProperty(name="Major Radius", default=TR_MJ_RD_DFLT, update=upd_tr)
    v_tr_mn: bpy.props.FloatProperty(name="Minor Radius", default=TR_MN_RD_DFLT, update=upd_tr)
    cl_tr: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TR_DFLT, min=0, max=1, update=upd_tr)
    
    b_cn: bpy.props.BoolProperty(default=False)
    lc_cn: bpy.props.FloatVectorProperty(name="Location", default=(CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT), update=upd_cn)
    rt_cn: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_cn)
    v_cn_tp: bpy.props.FloatProperty(name="Top Radius", default=CN_RD_TP_DFLT, update=upd_cn)
    v_cn_bt: bpy.props.FloatProperty(name="Bot Radius", default=CN_RD_BT_DFLT, update=upd_cn)
    v_cn_ht: bpy.props.FloatProperty(name="Height", default=CN_HT_DFLT, update=upd_cn)
    b_cn_cp: bpy.props.BoolProperty(name="No Caps", default=False, update=upd_cn)
    b_cn_fl: bpy.props.BoolProperty(name="Flip Norm", default=False, update=upd_cn)
    cl_cn_fr: bpy.props.FloatVectorProperty(name="Front Col", subtype='COLOR', size=4, default=COLOR_CN_FR_DFLT, min=0, max=1, update=upd_cn)
    cl_cn_bk: bpy.props.FloatVectorProperty(name="Back Col", subtype='COLOR', size=4, default=COLOR_CN_BK_DFLT, min=0, max=1, update=upd_cn)

    b_sp: bpy.props.BoolProperty(default=False)
    lc_sp: bpy.props.FloatVectorProperty(name="Location", default=(SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT), update=upd_sp)
    rt_sp: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_sp)
    v_sp_rd: bpy.props.FloatProperty(name="Radius", default=SP_RD_DFLT, update=upd_sp)
    cl_sp: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_SP_DFLT, min=0, max=1, update=upd_sp)

    b_aw: bpy.props.BoolProperty(default=False)
    v_aw_cn: bpy.props.IntProperty(name="Count", default=AW_CNT_DFLT, min=1, max=100, update=upd_aw)
    lc_aw_tg: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10), update=upd_aw)
    v_aw_of: bpy.props.FloatProperty(name="Offset %", default=100.0, min=1, max=100, update=upd_aw)
    v_aw_th: bpy.props.FloatProperty(name="Thickness", default=AW_THK_DFLT, update=upd_aw)
    cl_aw: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_AW_DFLT, min=0, max=1, update=upd_aw)

    b_dm: bpy.props.BoolProperty(default=False)
    s_dm_md: bpy.props.EnumProperty(
        items=[('HEMI', "半球 (Dome)", ""), ('FULL', "球体 (Sphere)", "")], 
        default=DM_MODE_DFLT, update=upd_dm
    )
    lc_dm: bpy.props.FloatVectorProperty(name="Location", default=(DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT), update=upd_dm)
    rt_dm: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_dm)
    v_dm_rd: bpy.props.FloatProperty(name="Radius", default=DM_RD_DFLT, update=upd_dm)
    v_dm_th: bpy.props.FloatProperty(name="Thickness", default=DM_THK_DFLT, update=upd_dm)
    v_dm_ln: bpy.props.IntProperty(name="Longt Count", default=12, update=upd_dm)
    v_dm_lt: bpy.props.IntProperty(name="Lat Count (per Hemi)", default=6, update=upd_dm)
    v_dm_sg: bpy.props.IntProperty(name="Segments", default=32, update=upd_dm)
    cl_dm: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_DM_DFLT, min=0, max=1, update=upd_dm)

    b_tx: bpy.props.BoolProperty(default=False)
    s_tx_bd: bpy.props.StringProperty(name="Text", default=TEXT_BODY_DFLT, update=upd_tx)
    v_tx_sz: bpy.props.FloatProperty(name="Size", default=TEXT_SIZE_DFLT, update=upd_tx)
    lc_tx: bpy.props.FloatVectorProperty(name="Location", default=(TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT), update=upd_tx)
    rt_tx: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(1.5708,0,0), update=upd_tx)
    cl_tx: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TX_DFLT, min=0, max=1, update=upd_tx)

# =========================================================
# 10. オペレーター群
# =========================================================
class Op_Cr_Tr(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_tr"
    bl_label = "トーラス生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_tr = True
        upd_tr(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Cn(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_cn"
    bl_label = "円錐台生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_cn = True
        upd_cn(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Sp(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_sp"
    bl_label = "球体生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_sp = True
        upd_sp(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Aw(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_aw"
    bl_label = "集中矢印生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_aw = True
        upd_aw(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Dm(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_dm"
    bl_label = "骨格生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_dm = True
        upd_dm(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}
        
class Op_Cr_Tx(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_tx"
    bl_label = "テキスト生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_tx = True
        upd_tx(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Dt_Tr(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_tr"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tr", "Detached_Torus", "b_tr")
        return {'FINISHED'}

class Op_Dt_Cn(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_cn"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Cn", "Detached_Cone", "b_cn")
        return {'FINISHED'}

class Op_Dt_Sp(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_sp"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Sp", "Detached_Sphere", "b_sp")
        return {'FINISHED'}

class Op_Dt_Aw(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_aw"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Aw", "Detached_Arrows", "b_aw")
        return {'FINISHED'}

class Op_Dt_Dm(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_dm"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Dm", "Detached_Bone", "b_dm")
        return {'FINISHED'}
        
class Op_Dt_Tx(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_tx"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tx", "Detached_Text", "b_tx")
        return {'FINISHED'}

class Op_Op_Ur(bpy.types.Operator):
    bl_idname = f"{PREFIX}.op_ur"
    bl_label = "Open URL"
    url: bpy.props.StringProperty()
    def execute(self, cx):
        webbrowser.open(self.url)
        return {'FINISHED'}

def dly_unrg():
    try: addon_utils.disable(__name__)
    except Exception: pass
    try: unregister()
    except Exception: pass
    return None

class Op_Rm_Ad(bpy.types.Operator):
    bl_idname = f"{PREFIX}.rm_ad"
    bl_label = "Clean up & Uninstall"
    def execute(self, cx):
        c_dt = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
        if c_dt:
            for ob in list(c_dt.objects): bpy.data.objects.remove(ob, do_unlink=True)
            bpy.data.collections.remove(c_dt)
        for m in list(bpy.data.meshes):
            if m.name.startswith(PREFIX_VAL): bpy.data.meshes.remove(m, do_unlink=True)
        for c in list(bpy.data.curves):
            if c.name.startswith(PREFIX_VAL): bpy.data.curves.remove(c, do_unlink=True)
        for mt in list(bpy.data.materials):
            if mt.name.startswith(PREFIX_VAL): bpy.data.materials.remove(mt, do_unlink=True)
        bpy.app.timers.register(dly_unrg, first_interval=0.2)
        return {'FINISHED'}

class Op_Xp_Tx(bpy.types.Operator):
    bl_idname = f"{PREFIX}.xp_tx"
    bl_label = "設定をコードに書き出し"
    def execute(self, cx):
        if not hasattr(cx.scene, "tc_g5_prp"): return {'CANCELLED'}
        p = cx.scene.tc_g5_prp
        ss = ""
        fp = globals().get("__file__")
        if fp and fp.endswith(".pyc"): fp = fp[:-1]
        if fp and os.path.exists(fp):
            try:
                with open(fp, "r", encoding="utf-8") as f: ss = f.read()
            except Exception: pass
        if not ss:
            for tx in bpy.data.texts:
                bd = tx.as_string()
                if ("# " + "--- DEFAULTS_BLOCK_START ---") in bd:
                    ss = bd
                    break
        if not ss: return {'CANCELLED'}
        
        ds = "# " + "--- DEFAULTS_BLOCK_START ---\n\n"
        ds += f"TR_MJ_RD_DFLT = {p.v_tr_mj}\nTR_MN_RD_DFLT = {p.v_tr_mn}\nCN_RD_TP_DFLT = {p.v_cn_tp}\nCN_RD_BT_DFLT = {p.v_cn_bt}\n"
        ds += f"CN_HT_DFLT = {p.v_cn_ht}\nSP_RD_DFLT = {p.v_sp_rd}\nAW_CNT_DFLT = {p.v_aw_cn}\nAW_THK_DFLT = {p.v_aw_th}\n"
        ds += f"DM_RD_DFLT = {p.v_dm_rd}\nDM_THK_DFLT = {p.v_dm_th}\n\n"
        ds += f"DM_MODE_DFLT = '{p.s_dm_md}'\n\n"
        ds += f"TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT = {p.lc_tr[0]:.2f}, {p.lc_tr[1]:.2f}, {p.lc_tr[2]:.2f}\n"
        ds += f"CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT = {p.lc_cn[0]:.2f}, {p.lc_cn[1]:.2f}, {p.lc_cn[2]:.2f}\n"
        ds += f"SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT = {p.lc_sp[0]:.2f}, {p.lc_sp[1]:.2f}, {p.lc_sp[2]:.2f}\n"
        ds += f"DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT = {p.lc_dm[0]:.2f}, {p.lc_dm[1]:.2f}, {p.lc_dm[2]:.2f}\n\n"
        ds += f"TEXT_BODY_DFLT = \"{p.s_tx_bd}\"\nTEXT_SIZE_DFLT = {p.v_tx_sz}\n"
        ds += f"TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT = {p.lc_tx[0]:.2f}, {p.lc_tx[1]:.2f}, {p.lc_tx[2]:.2f}\n\n"
        ds += f"COLOR_TR_DFLT = ({p.cl_tr[0]:.4f}, {p.cl_tr[1]:.4f}, {p.cl_tr[2]:.4f}, {p.cl_tr[3]:.4f})\n"
        ds += f"COLOR_CN_FR_DFLT = ({p.cl_cn_fr[0]:.4f}, {p.cl_cn_fr[1]:.4f}, {p.cl_cn_fr[2]:.4f}, {p.cl_cn_fr[3]:.4f})\n"
        ds += f"COLOR_CN_BK_DFLT = ({p.cl_cn_bk[0]:.4f}, {p.cl_cn_bk[1]:.4f}, {p.cl_cn_bk[2]:.4f}, {p.cl_cn_bk[3]:.4f})\n"
        ds += f"COLOR_SP_DFLT = ({p.cl_sp[0]:.4f}, {p.cl_sp[1]:.4f}, {p.cl_sp[2]:.4f}, {p.cl_sp[3]:.4f})\n"
        ds += f"COLOR_AW_DFLT = ({p.cl_aw[0]:.4f}, {p.cl_aw[1]:.4f}, {p.cl_aw[2]:.4f}, {p.cl_aw[3]:.4f})\n"
        ds += f"COLOR_DM_DFLT = ({p.cl_dm[0]:.4f}, {p.cl_dm[1]:.4f}, {p.cl_dm[2]:.4f}, {p.cl_dm[3]:.4f})\n"
        ds += f"COLOR_TX_DFLT = ({p.cl_tx[0]:.4f}, {p.cl_tx[1]:.4f}, {p.cl_tx[2]:.4f}, {p.cl_tx[3]:.4f})\n\n"
        
        m1 = "# " + "--- DEFAULTS_BLOCK_START ---"
        m2 = "# " + "--- DEFAULTS_BLOCK_END ---"
        pr = r"(" + m1 + r"\n)(.*?)(" + m2 + r")"
        ns = re.sub(pr, r"\g<1>" + ds + r"\g<3>", ss, flags=re.DOTALL)
        
        tn = "Exp_TCS_G5.py"
        nb = bpy.data.texts.get(tn)
        if nb: nb.clear()
        else: nb = bpy.data.texts.new(tn)
        nb.write(ns)
        
        sf = False
        for a in cx.screen.areas:
            if a.type == 'TEXT_EDITOR':
                a.spaces.active.text = nb
                sf = True
                break
        if sf: self.report({'INFO'}, f"書き出し成功: {tn}")
        else: self.report({'INFO'}, f"内部データに保存: {tn}")
        return {'FINISHED'}

# =========================================================
# 11. UIパネル
# =========================================================
class Pn_Bs(bpy.types.Panel):
    bl_space_type, bl_region_type, bl_category = 'VIEW_3D', 'UI', ADDON_CATEGORY

class Pn_Tr(Pn_Bs):
    bl_label, bl_idname, bl_order = "1. トーラス", f"{PREFIX_VAL}_P_Tr", 1
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_tr"); c.prop(p, "rt_tr"); c.separator()
        c.prop(p, "v_tr_mj"); c.prop(p, "v_tr_mn"); c.prop(p, "cl_tr"); ly.separator()
        r = ly.row(align=True)
        if not p.b_tr: r.operator(Op_Cr_Tr.bl_idname, icon='MESH_TORUS')
        else:
            r.operator(Op_Cr_Tr.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Tr.bl_idname, icon='UNLINKED')

class Pn_Cn(Pn_Bs):
    bl_label, bl_idname, bl_order = "2. 円錐台", f"{PREFIX_VAL}_P_Cn", 2
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_cn"); c.prop(p, "rt_cn"); c.separator()
        r1 = c.row(align=True); r1.prop(p, "v_cn_tp"); r1.prop(p, "v_cn_bt")
        c.prop(p, "v_cn_ht"); c.separator()
        r2 = c.row(align=True); r2.prop(p, "b_cn_cp"); r2.prop(p, "b_cn_fl"); c.separator()
        r3 = c.row(align=True); r3.prop(p, "cl_cn_fr", text="表面"); r3.prop(p, "cl_cn_bk", text="裏面"); ly.separator()
        r = ly.row(align=True)
        if not p.b_cn: r.operator(Op_Cr_Cn.bl_idname, icon='MESH_CONE')
        else:
            r.operator(Op_Cr_Cn.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Cn.bl_idname, icon='UNLINKED')

class Pn_Sp(Pn_Bs):
    bl_label, bl_idname, bl_order = "3. 球体", f"{PREFIX_VAL}_P_Sp", 3
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_sp"); c.prop(p, "rt_sp"); c.separator()
        c.prop(p, "v_sp_rd"); c.prop(p, "cl_sp"); ly.separator()
        r = ly.row(align=True)
        if not p.b_sp: r.operator(Op_Cr_Sp.bl_idname, icon='MESH_UVSPHERE')
        else:
            r.operator(Op_Cr_Sp.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Sp.bl_idname, icon='UNLINKED')

class Pn_Aw(Pn_Bs):
    bl_label, bl_idname, bl_order = "4. 集中矢印", f"{PREFIX_VAL}_P_Aw", 4
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "v_aw_cn"); c.prop(p, "lc_aw_tg"); c.prop(p, "v_aw_of"); c.separator()
        c.prop(p, "v_aw_th"); c.prop(p, "cl_aw"); ly.separator()
        r = ly.row(align=True)
        if not p.b_aw: r.operator(Op_Cr_Aw.bl_idname, icon='TRACKING')
        else:
            r.operator(Op_Cr_Aw.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Aw.bl_idname, icon='UNLINKED')

class Pn_Dm(Pn_Bs):
    bl_label, bl_idname, bl_order = "5. 骨格形状", f"{PREFIX_VAL}_P_Dm", 5
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "s_dm_md", text="形状")
        c.separator()
        c.prop(p, "lc_dm"); c.prop(p, "rt_dm"); c.separator()
        c.prop(p, "v_dm_rd"); c.prop(p, "v_dm_th"); c.separator()
        r1 = c.row(align=True); r1.prop(p, "v_dm_ln"); r1.prop(p, "v_dm_lt")
        c.prop(p, "v_dm_sg"); c.separator(); c.prop(p, "cl_dm"); ly.separator()
        r = ly.row(align=True)
        if not p.b_dm: r.operator(Op_Cr_Dm.bl_idname, icon='MESH_UVSPHERE')
        else:
            r.operator(Op_Cr_Dm.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Dm.bl_idname, icon='UNLINKED')

class Pn_Tx(Pn_Bs):
    bl_label, bl_idname, bl_order = "6. 3Dテキスト", f"{PREFIX_VAL}_P_Tx", 6
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "s_tx_bd"); c.prop(p, "v_tx_sz"); c.separator()
        c.prop(p, "lc_tx"); c.prop(p, "rt_tx"); c.separator(); c.prop(p, "cl_tx"); ly.separator()
        r = ly.row(align=True)
        if not p.b_tx: r.operator(Op_Cr_Tx.bl_idname, icon='FONT_DATA')
        else:
            r.operator(Op_Cr_Tx.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Tx.bl_idname, icon='UNLINKED')

class Pn_Lnk(Pn_Bs):
    bl_label, bl_idname, bl_order = "Links", f"{PREFIX_VAL}_P_Lnk", 10
    def draw(self, cx):
        b = self.layout.box()
        if LNK_NM_1: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_1, icon='URL').url = LNK_UR_1
        if LNK_NM_2: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_2, icon='URL').url = LNK_UR_2
        if LNK_NM_3: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_3, icon='URL').url = LNK_UR_3
        if LNK_NM_4: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_4, icon='URL').url = LNK_UR_4

class Pn_Rm(Pn_Bs):
    bl_label, bl_idname, bl_order, bl_options = "管理 & エクスポート", f"{PREFIX_VAL}_P_Rm", 11, {'DEFAULT_CLOSED'}
    def draw(self, cx):
        bx = self.layout.box()
        bx.operator(Op_Xp_Tx.bl_idname, icon='TEXT')
        bx.separator()
        bx.operator(Op_Rm_Ad.bl_idname, icon='CANCEL')

# =========================================================
# 12. 起動時自動展開処理
# =========================================================
def ui_st_up():
    try:
        cx = bpy.context
        if not cx or not cx.window_manager.windows: return 0.2
        for w in cx.window_manager.windows:
            for a in w.screen.areas:
                if a.type == 'VIEW_3D':
                    for s in a.spaces:
                        if s.type == 'VIEW_3D':
                            s.shading.type = 'MATERIAL'
                            s.show_region_ui = True
    except Exception: pass
    return None

cls_ls = [
    TC_G5_Prp, Op_Cr_Tr, Op_Cr_Cn, Op_Cr_Sp, Op_Cr_Aw, Op_Cr_Dm, Op_Cr_Tx, Op_Op_Ur,
    Op_Dt_Tr, Op_Dt_Cn, Op_Dt_Sp, Op_Dt_Aw, Op_Dt_Dm, Op_Dt_Tx,
    Op_Rm_Ad, Op_Xp_Tx,
    Pn_Tr, Pn_Cn, Pn_Sp, Pn_Aw, Pn_Dm, Pn_Tx, Pn_Lnk, Pn_Rm
]

def register():
    for c in cls_ls: bpy.utils.register_class(c)
    bpy.types.Scene.tc_g5_prp = bpy.props.PointerProperty(type=TC_G5_Prp)
    if not bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.register(ui_st_up, first_interval=0.2)

def unregister():
    try: del bpy.types.Scene.tc_g5_prp
    except Exception: pass
    for c in reversed(cls_ls):
        try: bpy.utils.unregister_class(c)
        except RuntimeError: pass
    if bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.unregister(ui_st_up)

if __name__ == "__main__": register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
    "name": "TCS & Arrows Generator 5.0",
    "author": "zionadchat",
    "version": (3, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar",
    "description": "Blender5専用: 自動UI展開・文字化け回避・エクスポートバグ対策版",
    "category": "3D View"
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1700

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import re
import os
import webbrowser
import addon_utils
from mathutils import Vector, Euler

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows 5"
PREFIX_VAL = "TCS_ARR_G5"
ADDON_CATEGORY = "TCS Gen 5.0"
PREFIX = PREFIX_VAL.lower()

# =========================================================
# 5. リンク定義
# =========================================================
LNK_NM_1 = "マニュアルページ"
LNK_UR_1 = "<https://example.com/manual>"
LNK_NM_2 = "コンテナ builder"
LNK_UR_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LNK_NM_3 = "posfie"
LNK_UR_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LNK_NM_4 = "zionadchat"
LNK_UR_4 = "<https://x.com/zionadchat>"

# =========================================================
# 6. 初期設定
# =========================================================
# --- DEFAULTS_BLOCK_START ---
# --- DEFAULTS_BLOCK_START ---

TR_MJ_RD_DFLT = 3.0
TR_MN_RD_DFLT = 0.20000000298023224
CN_RD_TP_DFLT = 0.0
CN_RD_BT_DFLT = 2.0
CN_HT_DFLT = 4.0
SP_RD_DFLT = 1.5
AW_CNT_DFLT = 8
AW_THK_DFLT = 0.10000000149011612
DM_RD_DFLT = 3.0
DM_THK_DFLT = 0.10000000149011612

TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT = 0.00, 0.00, 0.00
CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT = 5.00, 0.00, 0.00
SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT = -5.00, 0.00, 0.00
DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT = 0.00, 5.00, 0.00

TEXT_BODY_DFLT = "Sample Text"
TEXT_SIZE_DFLT = 1.5
TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT = 0.00, 0.00, 3.00

COLOR_TR_DFLT = (0.2000, 0.6000, 1.0000, 1.0000)
COLOR_CN_FR_DFLT = (1.0000, 0.5000, 0.1000, 1.0000)
COLOR_CN_BK_DFLT = (0.1000, 0.5000, 1.0000, 1.0000)
COLOR_SP_DFLT = (0.2000, 0.9000, 0.4000, 1.0000)
COLOR_AW_DFLT = (1.0000, 0.1000, 0.1000, 1.0000)
COLOR_DM_DFLT = (0.5729, 0.1036, 0.7038, 1.0000)
COLOR_TX_DFLT = (1.0000, 0.0376, 0.1255, 1.0000)

# --- DEFAULTS_BLOCK_END ---

# =========================================================
# 7. コアユーティリティ (文字化け回避対応済)
# =========================================================
def gt_cr_clc(nm_st):
    c_dt = bpy.data.collections.get(nm_st)
    if not c_dt:
        c_dt = bpy.data.collections.new(nm_st)
        bpy.context.scene.collection.children.link(c_dt)
    return c_dt

def gt_cr_mt(nm_st, cl_tp):
    m_dt = bpy.data.materials.get(nm_st)
    if not m_dt:
        m_dt = bpy.data.materials.new(nm_st)
        m_dt.use_nodes = True
        try:
            m_dt.blend_method = 'BLEND'
            m_dt.shadow_method = 'NONE'
        except Exception: pass
    if m_dt.use_nodes:
        bs_nd = m_dt.node_tree.nodes.get("Principled BSDF")
        if bs_nd:
            if "Base Color" in bs_nd.inputs: bs_nd.inputs["Base Color"].default_value = (cl_tp[0], cl_tp[1], cl_tp[2], 1.0)
            if "Alpha" in bs_nd.inputs: bs_nd.inputs["Alpha"].default_value = cl_tp[3]
    m_dt.diffuse_color = cl_tp
    return m_dt

def gt_cr_mt_dbl(nm_st, c_fr, c_bk):
    m_dt = bpy.data.materials.get(nm_st)
    if not m_dt:
        m_dt = bpy.data.materials.new(nm_st)
        m_dt.use_nodes = True
        try:
            m_dt.blend_method = 'BLEND'
            m_dt.shadow_method = 'NONE'
        except Exception: pass
        
    if m_dt.use_nodes:
        tr_dt = m_dt.node_tree
        bs_fr = tr_dt.nodes.get("Pr_Fr")
        if not bs_fr:
            tr_dt.nodes.clear()
            out_nd = tr_dt.nodes.new('ShaderNodeOutputMaterial')
            out_nd.location = (400, 0)
            mx_nd = tr_dt.nodes.new('ShaderNodeMixShader')
            mx_nd.location = (200, 0)
            bs_fr = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
            bs_fr.name = "Pr_Fr"
            bs_fr.location = (0, 150)
            bs_bk = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
            bs_bk.name = "Pr_Bk"
            bs_bk.location = (0, -200)
            gm_nd = tr_dt.nodes.new('ShaderNodeNewGeometry')
            gm_nd.location = (0, 400)
            
            tr_dt.links.new(gm_nd.outputs["Backfacing"], mx_nd.inputs[0])
            tr_dt.links.new(bs_fr.outputs["BSDF"], mx_nd.inputs[1])
            tr_dt.links.new(bs_bk.outputs["BSDF"], mx_nd.inputs[2])
            tr_dt.links.new(mx_nd.outputs["Shader"], out_nd.inputs["Surface"])
            
        bs_fr = tr_dt.nodes.get("Pr_Fr")
        bs_bk = tr_dt.nodes.get("Pr_Bk")
        
        if bs_fr:
            if "Base Color" in bs_fr.inputs: bs_fr.inputs["Base Color"].default_value = (c_fr[0], c_fr[1], c_fr[2], 1.0)
            if "Alpha" in bs_fr.inputs: bs_fr.inputs["Alpha"].default_value = c_fr[3]
        if bs_bk:
            if "Base Color" in bs_bk.inputs: bs_bk.inputs["Base Color"].default_value = (c_bk[0], c_bk[1], c_bk[2], 1.0)
            if "Alpha" in bs_bk.inputs: bs_bk.inputs["Alpha"].default_value = c_bk[3]
            
    m_dt.diffuse_color = c_fr
    return m_dt

def aply_dt(ob_nm, dt_fn, m_dt, dt_tp='MESH'):
    c_dt = gt_cr_clc(f"{PREFIX_VAL}_Col")
    ob_dt = bpy.data.objects.get(ob_nm)
    if ob_dt:
        od_dt = ob_dt.data
        ob_dt.data = dt_fn()
        if od_dt.users == 0:
            if dt_tp == 'MESH': bpy.data.meshes.remove(od_dt, do_unlink=True)
            elif dt_tp == 'CURVE': bpy.data.curves.remove(od_dt, do_unlink=True)
    else:
        nd_dt = dt_fn()
        ob_dt = bpy.data.objects.new(ob_nm, nd_dt)
        c_dt.objects.link(ob_dt)
        
    if not ob_dt.data.materials: ob_dt.data.materials.append(m_dt)
    else: ob_dt.data.materials[0] = m_dt
    return ob_dt

def dtch_ob(ctx, ob_nm, nw_nm, flg_st):
    ob_dt = bpy.data.objects.get(ob_nm)
    if not ob_dt: return False
    ts = time.strftime("%H%M%S")
    rn_nm = f"{nw_nm}_{ts}"
    ob_dt.name = rn_nm
    if ob_dt.data: ob_dt.data.name = f"{rn_nm}_Dt"
    for sl in ob_dt.material_slots:
        if sl.material and sl.material.name.startswith(PREFIX_VAL):
            nw_mt = sl.material.copy()
            nw_mt.name = f"{rn_nm}_Mt"
            sl.material = nw_mt
    m_cl = ctx.scene.collection
    if ob_dt.name not in m_cl.objects: m_cl.objects.link(ob_dt)
    a_cl = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
    if a_cl and ob_dt.name in a_cl.objects: a_cl.objects.unlink(ob_dt)
    setattr(ctx.scene.tc_g5_prp, flg_st, False)
    return True

# =========================================================
# 8. 更新処理 (Update)
# =========================================================
def upd_tr(sf, cx):
    if not sf.b_tr: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tr", sf.cl_tr)
    def mk_tr():
        v, f = [], []
        S, MS = 48, 16
        for i in range(S):
            a1 = (i/S)*2*math.pi
            c1, s1 = math.cos(a1), math.sin(a1)
            for j in range(MS):
                a2 = (j/MS)*2*math.pi
                c2, s2 = math.cos(a2), math.sin(a2)
                v.append(((sf.v_tr_mj + sf.v_tr_mn*c2)*c1, (sf.v_tr_mj + sf.v_tr_mn*c2)*s1, sf.v_tr_mn*s2))
        for i in range(S):
            for j in range(MS):
                i_n, j_n = (i+1)%S, (j+1)%MS
                f.append((i*MS+j, i_n*MS+j, i_n*MS+j_n, i*MS+j_n))
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Tr")
        me.from_pydata(v, [], f)
        me.update()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Tr", mk_tr, m_dt)
    ob.location, ob.rotation_euler = sf.lc_tr, sf.rt_tr
    if sf.b_aw: upd_aw(sf, cx)

def upd_cn(sf, cx):
    if not sf.b_cn: return
    m_dt = gt_cr_mt_dbl(f"{PREFIX_VAL}_Mt_Cn", sf.cl_cn_fr, sf.cl_cn_bk)
    def mk_cn():
        bm = bmesh.new()
        ah = max(abs(sf.v_cn_ht), 0.001)
        r1, r2, zo = (sf.v_cn_bt, sf.v_cn_tp, ah/2) if sf.v_cn_ht >= 0 else (sf.v_cn_tp, sf.v_cn_bt, -ah/2)
        bmesh.ops.create_cone(bm, cap_ends=not sf.b_cn_cp, cap_tris=True, segments=48, radius1=r1, radius2=r2, depth=ah)
        bmesh.ops.translate(bm, verts=bm.verts, vec=(0,0,zo))
        if sf.b_cn_fl: bmesh.ops.reverse_faces(bm, faces=bm.faces)
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Cn")
        bm.to_mesh(me)
        bm.free()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Cn", mk_cn, m_dt)
    ob.location, ob.rotation_euler = sf.lc_cn, sf.rt_cn

def upd_sp(sf, cx):
    if not sf.b_sp: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Sp", sf.cl_sp)
    def mk_sp():
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=sf.v_sp_rd)
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Sp")
        bm.to_mesh(me)
        bm.free()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Sp", mk_sp, m_dt)
    ob.location, ob.rotation_euler = sf.lc_sp, sf.rt_sp

def upd_aw(sf, cx):
    if not sf.b_aw: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Aw", sf.cl_aw)
    def mk_aw():
        bm = bmesh.new()
        tl, tm = Vector(sf.lc_tr), Euler(sf.rt_tr).to_matrix()
        for i in range(sf.v_aw_cn):
            th = i*(2*math.pi/sf.v_aw_cn)
            sp = tl + tm @ Vector((sf.v_tr_mj*math.cos(th), sf.v_tr_mj*math.sin(th), 0))
            vc = Vector(sf.lc_aw_tg) - sp
            ds = vc.length * (sf.v_aw_of/100.0)
            if ds < 0.001: continue
            hl = min(sf.v_aw_th*4.0, ds*0.5)
            sl = ds - hl
            rq = Vector((0,0,1)).rotation_difference(vc.normalized())
            gs = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th, radius2=sf.v_aw_th, depth=sl)
            bmesh.ops.translate(bm, verts=gs['verts'], vec=(0,0,sl/2))
            gh = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th*2.5, radius2=0, depth=hl)
            bmesh.ops.translate(bm, verts=gh['verts'], vec=(0,0,sl+hl/2))
            va = gs['verts'] + gh['verts']
            bmesh.ops.transform(bm, verts=va, matrix=rq.to_matrix().to_4x4())
            bmesh.ops.translate(bm, verts=va, vec=sp)
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Aw")
        bm.to_mesh(me)
        bm.free()
        return me
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Aw", mk_aw, m_dt)
    ob.location, ob.rotation_euler = (0,0,0), (0,0,0)

def upd_dm(sf, cx):
    if not sf.b_dm: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Dm", sf.cl_dm)
    def mk_dm():
        cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Dm", 'CURVE')
        cv.dimensions, cv.fill_mode = '3D', 'FULL'
        cv.bevel_depth, cv.bevel_resolution = sf.v_dm_th, 4
        for i in range(sf.v_dm_ln):
            th = (2*math.pi/sf.v_dm_ln)*i
            sl = cv.splines.new('POLY')
            sl.points.add(sf.v_dm_sg - 1)
            for j in range(sf.v_dm_sg):
                ph = (math.pi/2)*(j/(sf.v_dm_sg - 1))
                sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th), sf.v_dm_rd*math.sin(ph)*math.sin(th), sf.v_dm_rd*math.cos(ph), 1)
        for i in range(1, sf.v_dm_lt + 1):
            ph = (math.pi/2)*(i/sf.v_dm_lt)
            sl = cv.splines.new('POLY')
            sl.points.add(sf.v_dm_sg - 1)
            for j in range(sf.v_dm_sg):
                th = (2*math.pi)*(j/(sf.v_dm_sg - 1))
                sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th), sf.v_dm_rd*math.sin(ph)*math.sin(th), sf.v_dm_rd*math.cos(ph), 1)
            sl.use_cyclic_u = True
        return cv
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Dm", mk_dm, m_dt, 'CURVE')
    ob.location, ob.rotation_euler = sf.lc_dm, sf.rt_dm

def upd_tx(sf, cx):
    if not sf.b_tx: return
    m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tx", sf.cl_tx)
    def mk_tx():
        cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Tx", 'FONT')
        cv.body = sf.s_tx_bd
        cv.size = sf.v_tx_sz
        cv.extrude = 0.05
        return cv
    ob = aply_dt(f"{PREFIX_VAL}_Ob_Tx", mk_tx, m_dt, 'CURVE')
    ob.location = sf.lc_tx
    ob.rotation_euler = sf.rt_tx

# =========================================================
# 9. プロパティ定義
# =========================================================
class TC_G5_Prp(bpy.types.PropertyGroup):
    b_tr: bpy.props.BoolProperty(default=False)
    lc_tr: bpy.props.FloatVectorProperty(name="Location", default=(TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT), update=upd_tr)
    rt_tr: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_tr)
    v_tr_mj: bpy.props.FloatProperty(name="Major Radius", default=TR_MJ_RD_DFLT, update=upd_tr)
    v_tr_mn: bpy.props.FloatProperty(name="Minor Radius", default=TR_MN_RD_DFLT, update=upd_tr)
    cl_tr: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TR_DFLT, min=0, max=1, update=upd_tr)
    
    b_cn: bpy.props.BoolProperty(default=False)
    lc_cn: bpy.props.FloatVectorProperty(name="Location", default=(CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT), update=upd_cn)
    rt_cn: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_cn)
    v_cn_tp: bpy.props.FloatProperty(name="Top Radius", default=CN_RD_TP_DFLT, update=upd_cn)
    v_cn_bt: bpy.props.FloatProperty(name="Bot Radius", default=CN_RD_BT_DFLT, update=upd_cn)
    v_cn_ht: bpy.props.FloatProperty(name="Height", default=CN_HT_DFLT, update=upd_cn)
    b_cn_cp: bpy.props.BoolProperty(name="No Caps", default=False, update=upd_cn)
    b_cn_fl: bpy.props.BoolProperty(name="Flip Norm", default=False, update=upd_cn)
    cl_cn_fr: bpy.props.FloatVectorProperty(name="Front Col", subtype='COLOR', size=4, default=COLOR_CN_FR_DFLT, min=0, max=1, update=upd_cn)
    cl_cn_bk: bpy.props.FloatVectorProperty(name="Back Col", subtype='COLOR', size=4, default=COLOR_CN_BK_DFLT, min=0, max=1, update=upd_cn)

    b_sp: bpy.props.BoolProperty(default=False)
    lc_sp: bpy.props.FloatVectorProperty(name="Location", default=(SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT), update=upd_sp)
    rt_sp: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_sp)
    v_sp_rd: bpy.props.FloatProperty(name="Radius", default=SP_RD_DFLT, update=upd_sp)
    cl_sp: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_SP_DFLT, min=0, max=1, update=upd_sp)

    b_aw: bpy.props.BoolProperty(default=False)
    v_aw_cn: bpy.props.IntProperty(name="Count", default=AW_CNT_DFLT, min=1, max=100, update=upd_aw)
    lc_aw_tg: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10), update=upd_aw)
    v_aw_of: bpy.props.FloatProperty(name="Offset %", default=100.0, min=1, max=100, update=upd_aw)
    v_aw_th: bpy.props.FloatProperty(name="Thickness", default=AW_THK_DFLT, update=upd_aw)
    cl_aw: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_AW_DFLT, min=0, max=1, update=upd_aw)

    b_dm: bpy.props.BoolProperty(default=False)
    lc_dm: bpy.props.FloatVectorProperty(name="Location", default=(DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT), update=upd_dm)
    rt_dm: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_dm)
    v_dm_rd: bpy.props.FloatProperty(name="Radius", default=DM_RD_DFLT, update=upd_dm)
    v_dm_th: bpy.props.FloatProperty(name="Thickness", default=DM_THK_DFLT, update=upd_dm)
    v_dm_ln: bpy.props.IntProperty(name="Longt Count", default=12, update=upd_dm)
    v_dm_lt: bpy.props.IntProperty(name="Lat Count", default=6, update=upd_dm)
    v_dm_sg: bpy.props.IntProperty(name="Segments", default=32, update=upd_dm)
    cl_dm: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_DM_DFLT, min=0, max=1, update=upd_dm)

    b_tx: bpy.props.BoolProperty(default=False)
    s_tx_bd: bpy.props.StringProperty(name="Text", default=TEXT_BODY_DFLT, update=upd_tx)
    v_tx_sz: bpy.props.FloatProperty(name="Size", default=TEXT_SIZE_DFLT, update=upd_tx)
    lc_tx: bpy.props.FloatVectorProperty(name="Location", default=(TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT), update=upd_tx)
    rt_tx: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(1.5708,0,0), update=upd_tx)
    cl_tx: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TX_DFLT, min=0, max=1, update=upd_tx)

# =========================================================
# 10. オペレーター群
# =========================================================
class Op_Cr_Tr(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_tr"
    bl_label = "トーラス生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_tr = True
        upd_tr(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Cn(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_cn"
    bl_label = "円錐台生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_cn = True
        upd_cn(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Sp(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_sp"
    bl_label = "球体生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_sp = True
        upd_sp(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Aw(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_aw"
    bl_label = "集中矢印生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_aw = True
        upd_aw(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Cr_Dm(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_dm"
    bl_label = "骨格ドーム生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_dm = True
        upd_dm(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}
        
class Op_Cr_Tx(bpy.types.Operator):
    bl_idname = f"{PREFIX}.cr_tx"
    bl_label = "テキスト生成"
    def execute(self, cx):
        cx.scene.tc_g5_prp.b_tx = True
        upd_tx(cx.scene.tc_g5_prp, cx)
        return {'FINISHED'}

class Op_Dt_Tr(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_tr"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tr", "Detached_Torus", "b_tr")
        return {'FINISHED'}

class Op_Dt_Cn(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_cn"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Cn", "Detached_Cone", "b_cn")
        return {'FINISHED'}

class Op_Dt_Sp(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_sp"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Sp", "Detached_Sphere", "b_sp")
        return {'FINISHED'}

class Op_Dt_Aw(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_aw"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Aw", "Detached_Arrows", "b_aw")
        return {'FINISHED'}

class Op_Dt_Dm(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_dm"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Dm", "Detached_Dome", "b_dm")
        return {'FINISHED'}
        
class Op_Dt_Tx(bpy.types.Operator):
    bl_idname = f"{PREFIX}.dt_tx"
    bl_label = "独立"
    def execute(self, cx):
        dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tx", "Detached_Text", "b_tx")
        return {'FINISHED'}

def dly_unrg():
    try: addon_utils.disable(__name__)
    except Exception: pass
    try: unregister()
    except Exception: pass
    return None

class Op_Rm_Ad(bpy.types.Operator):
    bl_idname = f"{PREFIX}.rm_ad"
    bl_label = "Clean up & Uninstall"
    def execute(self, cx):
        c_dt = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
        if c_dt:
            for ob in list(c_dt.objects): bpy.data.objects.remove(ob, do_unlink=True)
            bpy.data.collections.remove(c_dt)
        for m in list(bpy.data.meshes):
            if m.name.startswith(PREFIX_VAL): bpy.data.meshes.remove(m, do_unlink=True)
        for c in list(bpy.data.curves):
            if c.name.startswith(PREFIX_VAL): bpy.data.curves.remove(c, do_unlink=True)
        for mt in list(bpy.data.materials):
            if mt.name.startswith(PREFIX_VAL): bpy.data.materials.remove(mt, do_unlink=True)
        bpy.app.timers.register(dly_unrg, first_interval=0.2)
        return {'FINISHED'}

class Op_Xp_Tx(bpy.types.Operator):
    bl_idname = f"{PREFIX}.xp_tx"
    bl_label = "設定をコードに書き出し"
    def execute(self, cx):
        if not hasattr(cx.scene, "tc_g5_prp"): return {'CANCELLED'}
        p = cx.scene.tc_g5_prp
        ss = ""
        fp = globals().get("__file__")
        if fp and fp.endswith(".pyc"): fp = fp[:-1]
        if fp and os.path.exists(fp):
            try:
                with open(fp, "r", encoding="utf-8") as f: ss = f.read()
            except Exception: pass
        if not ss:
            for tx in bpy.data.texts:
                bd = tx.as_string()
                # 誤爆防止のため文字列を分割結合して判定
                if ("# " + "--- DEFAULTS_BLOCK_START ---") in bd:
                    ss = bd
                    break
        if not ss: return {'CANCELLED'}
        
        ds = "# " + "--- DEFAULTS_BLOCK_START ---\n\n"
        ds += f"TR_MJ_RD_DFLT = {p.v_tr_mj}\nTR_MN_RD_DFLT = {p.v_tr_mn}\nCN_RD_TP_DFLT = {p.v_cn_tp}\nCN_RD_BT_DFLT = {p.v_cn_bt}\n"
        ds += f"CN_HT_DFLT = {p.v_cn_ht}\nSP_RD_DFLT = {p.v_sp_rd}\nAW_CNT_DFLT = {p.v_aw_cn}\nAW_THK_DFLT = {p.v_aw_th}\n"
        ds += f"DM_RD_DFLT = {p.v_dm_rd}\nDM_THK_DFLT = {p.v_dm_th}\n\n"
        ds += f"TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT = {p.lc_tr[0]:.2f}, {p.lc_tr[1]:.2f}, {p.lc_tr[2]:.2f}\n"
        ds += f"CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT = {p.lc_cn[0]:.2f}, {p.lc_cn[1]:.2f}, {p.lc_cn[2]:.2f}\n"
        ds += f"SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT = {p.lc_sp[0]:.2f}, {p.lc_sp[1]:.2f}, {p.lc_sp[2]:.2f}\n"
        ds += f"DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT = {p.lc_dm[0]:.2f}, {p.lc_dm[1]:.2f}, {p.lc_dm[2]:.2f}\n\n"
        ds += f"TEXT_BODY_DFLT = \"{p.s_tx_bd}\"\nTEXT_SIZE_DFLT = {p.v_tx_sz}\n"
        ds += f"TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT = {p.lc_tx[0]:.2f}, {p.lc_tx[1]:.2f}, {p.lc_tx[2]:.2f}\n\n"
        ds += f"COLOR_TR_DFLT = ({p.cl_tr[0]:.4f}, {p.cl_tr[1]:.4f}, {p.cl_tr[2]:.4f}, {p.cl_tr[3]:.4f})\n"
        ds += f"COLOR_CN_FR_DFLT = ({p.cl_cn_fr[0]:.4f}, {p.cl_cn_fr[1]:.4f}, {p.cl_cn_fr[2]:.4f}, {p.cl_cn_fr[3]:.4f})\n"
        ds += f"COLOR_CN_BK_DFLT = ({p.cl_cn_bk[0]:.4f}, {p.cl_cn_bk[1]:.4f}, {p.cl_cn_bk[2]:.4f}, {p.cl_cn_bk[3]:.4f})\n"
        ds += f"COLOR_SP_DFLT = ({p.cl_sp[0]:.4f}, {p.cl_sp[1]:.4f}, {p.cl_sp[2]:.4f}, {p.cl_sp[3]:.4f})\n"
        ds += f"COLOR_AW_DFLT = ({p.cl_aw[0]:.4f}, {p.cl_aw[1]:.4f}, {p.cl_aw[2]:.4f}, {p.cl_aw[3]:.4f})\n"
        ds += f"COLOR_DM_DFLT = ({p.cl_dm[0]:.4f}, {p.cl_dm[1]:.4f}, {p.cl_dm[2]:.4f}, {p.cl_dm[3]:.4f})\n"
        ds += f"COLOR_TX_DFLT = ({p.cl_tx[0]:.4f}, {p.cl_tx[1]:.4f}, {p.cl_tx[2]:.4f}, {p.cl_tx[3]:.4f})\n\n"
        
        # エラー回避のための正規表現パターン分割
        m1 = "# " + "--- DEFAULTS_BLOCK_START ---"
        m2 = "# " + "--- DEFAULTS_BLOCK_END ---"
        pr = r"(" + m1 + r"\n)(.*?)(" + m2 + r")"
        ns = re.sub(pr, r"\g<1>" + ds + r"\g<3>", ss, flags=re.DOTALL)
        
        tn = "Exp_TCS_G5.py"
        nb = bpy.data.texts.get(tn)
        if nb: nb.clear()
        else: nb = bpy.data.texts.new(tn)
        nb.write(ns)
        
        sf = False
        for a in cx.screen.areas:
            if a.type == 'TEXT_EDITOR':
                a.spaces.active.text = nb
                sf = True
                break
        if sf: self.report({'INFO'}, f"書き出し成功: {tn}")
        else: self.report({'INFO'}, f"内部データに保存: {tn}")
        return {'FINISHED'}

# =========================================================
# 11. UIパネル
# =========================================================
class Pn_Bs(bpy.types.Panel):
    bl_space_type, bl_region_type, bl_category = 'VIEW_3D', 'UI', ADDON_CATEGORY

class Pn_Tr(Pn_Bs):
    bl_label, bl_idname, bl_order = "1. トーラス", f"{PREFIX_VAL}_P_Tr", 1
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_tr"); c.prop(p, "rt_tr"); c.separator()
        c.prop(p, "v_tr_mj"); c.prop(p, "v_tr_mn"); c.prop(p, "cl_tr"); ly.separator()
        r = ly.row(align=True)
        if not p.b_tr: r.operator(Op_Cr_Tr.bl_idname, icon='MESH_TORUS')
        else:
            r.operator(Op_Cr_Tr.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Tr.bl_idname, icon='UNLINKED')

class Pn_Cn(Pn_Bs):
    bl_label, bl_idname, bl_order = "2. 円錐台", f"{PREFIX_VAL}_P_Cn", 2
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_cn"); c.prop(p, "rt_cn"); c.separator()
        r1 = c.row(align=True); r1.prop(p, "v_cn_tp"); r1.prop(p, "v_cn_bt")
        c.prop(p, "v_cn_ht"); c.separator()
        r2 = c.row(align=True); r2.prop(p, "b_cn_cp"); r2.prop(p, "b_cn_fl"); c.separator()
        r3 = c.row(align=True); r3.prop(p, "cl_cn_fr", text="表面"); r3.prop(p, "cl_cn_bk", text="裏面"); ly.separator()
        r = ly.row(align=True)
        if not p.b_cn: r.operator(Op_Cr_Cn.bl_idname, icon='MESH_CONE')
        else:
            r.operator(Op_Cr_Cn.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Cn.bl_idname, icon='UNLINKED')

class Pn_Sp(Pn_Bs):
    bl_label, bl_idname, bl_order = "3. 球体", f"{PREFIX_VAL}_P_Sp", 3
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_sp"); c.prop(p, "rt_sp"); c.separator()
        c.prop(p, "v_sp_rd"); c.prop(p, "cl_sp"); ly.separator()
        r = ly.row(align=True)
        if not p.b_sp: r.operator(Op_Cr_Sp.bl_idname, icon='MESH_UVSPHERE')
        else:
            r.operator(Op_Cr_Sp.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Sp.bl_idname, icon='UNLINKED')

class Pn_Aw(Pn_Bs):
    bl_label, bl_idname, bl_order = "4. 集中矢印", f"{PREFIX_VAL}_P_Aw", 4
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "v_aw_cn"); c.prop(p, "lc_aw_tg"); c.prop(p, "v_aw_of"); c.separator()
        c.prop(p, "v_aw_th"); c.prop(p, "cl_aw"); ly.separator()
        r = ly.row(align=True)
        if not p.b_aw: r.operator(Op_Cr_Aw.bl_idname, icon='TRACKING')
        else:
            r.operator(Op_Cr_Aw.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Aw.bl_idname, icon='UNLINKED')

class Pn_Dm(Pn_Bs):
    bl_label, bl_idname, bl_order = "5. 骨格ドーム", f"{PREFIX_VAL}_P_Dm", 5
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "lc_dm"); c.prop(p, "rt_dm"); c.separator()
        c.prop(p, "v_dm_rd"); c.prop(p, "v_dm_th"); c.separator()
        r1 = c.row(align=True); r1.prop(p, "v_dm_ln"); r1.prop(p, "v_dm_lt")
        c.prop(p, "v_dm_sg"); c.separator(); c.prop(p, "cl_dm"); ly.separator()
        r = ly.row(align=True)
        if not p.b_dm: r.operator(Op_Cr_Dm.bl_idname, icon='MESH_UVSPHERE')
        else:
            r.operator(Op_Cr_Dm.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Dm.bl_idname, icon='UNLINKED')

class Pn_Tx(Pn_Bs):
    bl_label, bl_idname, bl_order = "6. 3Dテキスト", f"{PREFIX_VAL}_P_Tx", 6
    def draw(self, cx):
        ly, p = self.layout, cx.scene.tc_g5_prp
        c = ly.column(align=True)
        c.prop(p, "s_tx_bd"); c.prop(p, "v_tx_sz"); c.separator()
        c.prop(p, "lc_tx"); c.prop(p, "rt_tx"); c.separator(); c.prop(p, "cl_tx"); ly.separator()
        r = ly.row(align=True)
        if not p.b_tx: r.operator(Op_Cr_Tx.bl_idname, icon='FONT_DATA')
        else:
            r.operator(Op_Cr_Tx.bl_idname, text="更新", icon='FILE_REFRESH')
            r.operator(Op_Dt_Tx.bl_idname, icon='UNLINKED')

class Pn_Rm(Pn_Bs):
    bl_label, bl_idname, bl_order, bl_options = "管理 & エクスポート", f"{PREFIX_VAL}_P_Rm", 10, {'DEFAULT_CLOSED'}
    def draw(self, cx):
        bx = self.layout.box()
        bx.operator(Op_Xp_Tx.bl_idname, icon='TEXT')
        bx.separator()
        bx.operator(Op_Rm_Ad.bl_idname, icon='CANCEL')

# =========================================================
# 12. 起動時自動展開処理
# =========================================================
def ui_st_up():
    try:
        cx = bpy.context
        if not cx or not cx.window_manager.windows: return 0.2
        for w in cx.window_manager.windows:
            for a in w.screen.areas:
                if a.type == 'VIEW_3D':
                    for s in a.spaces:
                        if s.type == 'VIEW_3D':
                            s.shading.type = 'MATERIAL'
                            s.show_region_ui = True
    except Exception: pass
    return None

cls_ls = [
    TC_G5_Prp, Op_Cr_Tr, Op_Cr_Cn, Op_Cr_Sp, Op_Cr_Aw, Op_Cr_Dm, Op_Cr_Tx,
    Op_Dt_Tr, Op_Dt_Cn, Op_Dt_Sp, Op_Dt_Aw, Op_Dt_Dm, Op_Dt_Tx,
    Op_Rm_Ad, Op_Xp_Tx,
    Pn_Tr, Pn_Cn, Pn_Sp, Pn_Aw, Pn_Dm, Pn_Tx, Pn_Rm
]

def register():
    for c in cls_ls: bpy.utils.register_class(c)
    bpy.types.Scene.tc_g5_prp = bpy.props.PointerProperty(type=TC_G5_Prp)
    if not bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.register(ui_st_up, first_interval=0.2)

def unregister():
    try: del bpy.types.Scene.tc_g5_prp
    except Exception: pass
    for c in reversed(cls_ls):
        try: bpy.utils.unregister_class(c)
        except RuntimeError: pass
    if bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.unregister(ui_st_up)

if __name__ == "__main__": register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
    "name": "TCS & Arrows Generator",
    "author": "Your Name",
    "version": (2, 6, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar",
    "description": "トーラス・円錐台・球体・集中矢印・骨格半球ドーム(裏面色・透明度完全対応・汎用円錐台)",
    "category": "3D View"
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1630

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import webbrowser
import addon_utils
from mathutils import Vector, Euler

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows"
PREFIX_VAL = "TCS_ARR"
ADDON_CATEGORY = "TCS Gen"
PREFIX = PREFIX_VAL.lower()

# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "マニュアルページ"
LINK_URL_1 = "<https://example.com/manual>"

LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"

LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

LINK_NAME_5 = ""
LINK_URL_5 = ""

# =========================================================
# 6. 共通ユーティリティ
# =========================================================
class TCS_ARR_Utility:
    @staticmethod
    def get_or_create_collection(name):
        col = bpy.data.collections.get(name)
        if not col:
            col = bpy.data.collections.new(name)
            bpy.context.scene.collection.children.link(col)
        return col

    @staticmethod
    def get_or_create_material(name, color):
        """通常の単一マテリアル生成"""
        mat = bpy.data.materials.get(name)
        if not mat:
            mat = bpy.data.materials.new(name)
            mat.use_nodes = True
            
        try:
            if hasattr(mat, "blend_method"):
                mat.blend_method = 'BLEND'
            if hasattr(mat, "shadow_method"):
                mat.shadow_method = 'NONE'
        except Exception:
            pass

        if mat.use_nodes:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                if "Base Color" in bsdf.inputs:
                    bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)
                if "Alpha" in bsdf.inputs:
                    bsdf.inputs["Alpha"].default_value = color[3]
                    
        mat.diffuse_color = (color[0], color[1], color[2], color[3])
        return mat

    @staticmethod
    def get_or_create_double_sided_material(name, front_color, back_color):
        """表裏で色・透明度を分ける両面マテリアル生成(円錐用)"""
        mat = bpy.data.materials.get(name)
        if not mat:
            mat = bpy.data.materials.new(name)
            mat.use_nodes = True
            
        try:
            if hasattr(mat, "blend_method"):
                mat.blend_method = 'BLEND'
            if hasattr(mat, "shadow_method"):
                mat.shadow_method = 'NONE'
        except Exception:
            pass

        if mat.use_nodes:
            tree = mat.node_tree
            bsdf_front = tree.nodes.get("Principled BSDF Front")
            
            if not bsdf_front:
                tree.nodes.clear()
                
                out_node = tree.nodes.new(type='ShaderNodeOutputMaterial')
                out_node.location = (400, 0)
                
                mix_shader = tree.nodes.new(type='ShaderNodeMixShader')
                mix_shader.location = (200, 0)
                
                bsdf_front = tree.nodes.new(type='ShaderNodeBsdfPrincipled')
                bsdf_front.name = "Principled BSDF Front"
                bsdf_front.label = "Front Color"
                bsdf_front.location = (0, 150)
                
                bsdf_back = tree.nodes.new(type='ShaderNodeBsdfPrincipled')
                bsdf_back.name = "Principled BSDF Back"
                bsdf_back.label = "Back Color"
                bsdf_back.location = (0, -200)
                
                geom_node = tree.nodes.new(type='ShaderNodeNewGeometry')
                geom_node.location = (0, 400)
                
                tree.links.new(geom_node.outputs["Backfacing"], mix_shader.inputs[0])
                tree.links.new(bsdf_front.outputs["BSDF"], mix_shader.inputs[1])
                tree.links.new(bsdf_back.outputs["BSDF"], mix_shader.inputs[2])
                tree.links.new(mix_shader.outputs["Shader"], out_node.inputs["Surface"])

            bsdf_front = tree.nodes.get("Principled BSDF Front")
            bsdf_back = tree.nodes.get("Principled BSDF Back")

            if bsdf_front:
                if "Base Color" in bsdf_front.inputs:
                    bsdf_front.inputs["Base Color"].default_value = (front_color[0], front_color[1], front_color[2], 1.0)
                if "Alpha" in bsdf_front.inputs:
                    bsdf_front.inputs["Alpha"].default_value = front_color[3]
                    
            if bsdf_back:
                if "Base Color" in bsdf_back.inputs:
                    bsdf_back.inputs["Base Color"].default_value = (back_color[0], back_color[1], back_color[2], 1.0)
                if "Alpha" in bsdf_back.inputs:
                    bsdf_back.inputs["Alpha"].default_value = back_color[3]
                
        mat.diffuse_color = (front_color[0], front_color[1], front_color[2], front_color[3])
        return mat

    @staticmethod
    def apply_mesh(obj_name, mesh_func, mat):
        col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
        obj = bpy.data.objects.get(obj_name)
        if obj:
            old_mesh = obj.data
            obj.data = mesh_func()
            if old_mesh.users == 0:
                bpy.data.meshes.remove(old_mesh, do_unlink=True)
        else:
            mesh = mesh_func()
            obj = bpy.data.objects.new(obj_name, mesh)
            col.objects.link(obj)
            
        if len(obj.data.materials) == 0:
            obj.data.materials.append(mat)
        else:
            obj.data.materials[0] = mat
        return obj

    @staticmethod
    def apply_curve(obj_name, curve_func, mat):
        """カーブオブジェクト用の適用処理(ドーム用)"""
        col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
        obj = bpy.data.objects.get(obj_name)
        if obj:
            old_curve = obj.data
            obj.data = curve_func()
            if old_curve.users == 0:
                bpy.data.curves.remove(old_curve, do_unlink=True)
        else:
            curve = curve_func()
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
            
        if len(obj.data.materials) == 0:
            obj.data.materials.append(mat)
        else:
            obj.data.materials[0] = mat
        return obj

    @staticmethod
    def detach_object(context, obj_name, base_new_name, flag_attr):
        obj = bpy.data.objects.get(obj_name)
        if not obj:
            return False
            
        ts_str = time.strftime("%H%M%S")
        new_name = f"{base_new_name}_{ts_str}"
        obj.name = new_name
        
        # Mesh/Curve 両対応のための名称変更
        if obj.data:
            obj.data.name = f"{new_name}_Data"
            
        for slot in obj.material_slots:
            if slot.material and slot.material.name.startswith(PREFIX_VAL):
                new_mat = slot.material.copy()
                new_mat.name = f"{new_name}_Mat"
                slot.material = new_mat
                
        main_col = context.scene.collection
        if obj.name not in main_col.objects:
            main_col.objects.link(obj)
            
        addon_col = bpy.data.collections.get(f"{PREFIX_VAL}_Collection")
        if addon_col and obj.name in addon_col.objects:
            addon_col.objects.unlink(obj)
            
        setattr(context.scene.tcs_arr_props, flag_attr, False)
        return True

# =========================================================
# 7. 更新処理 (Update Functions)
# =========================================================
def update_torus(self, context):
    if not self.is_tr_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Torus", self.tr_cl)
    
    def make_torus():
        major_r = self.tr_mj_rd
        minor_r = self.tr_mn_rd
        segments, minor_segments = 48, 16
        verts, faces = [], []
        
        for i in range(segments):
            a1 = (i / segments) * 2.0 * math.pi
            c1, s1 = math.cos(a1), math.sin(a1)
            for j in range(minor_segments):
                a2 = (j / minor_segments) * 2.0 * math.pi
                c2, s2 = math.cos(a2), math.sin(a2)
                x = (major_r + minor_r * c2) * c1
                y = (major_r + minor_r * c2) * s1
                z = minor_r * s2
                verts.append((x, y, z))
                
        for i in range(segments):
            for j in range(minor_segments):
                i_next = (i + 1) % segments
                j_next = (j + 1) % minor_segments
                v0 = i * minor_segments + j
                v1 = i_next * minor_segments + j
                v2 = i_next * minor_segments + j_next
                v3 = i * minor_segments + j_next
                faces.append((v0, v1, v2, v3))
                
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Torus_Mesh")
        me.from_pydata(verts, [], faces)
        me.update()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Torus", make_torus, mat)
    obj.location = self.tr_loc
    obj.rotation_euler = self.tr_rot
    
    if self.is_aw_created:
        update_arrows(self, context)

def update_cone(self, context):
    if not self.is_cn_created: return
    mat = TCS_ARR_Utility.get_or_create_double_sided_material(
        f"{PREFIX_VAL}_Mat_Cone", self.cn_cl, self.cn_cl_back
    )
    
    def make_cone():
        bm = bmesh.new()
        ht = self.cn_ht
        abs_ht = abs(ht) if abs(ht) > 0.001 else 0.001
        
        if ht >= 0:
            r1 = self.cn_rd_bottom
            r2 = self.cn_rd_top
            z_offset = abs_ht / 2.0
        else:
            r1 = self.cn_rd_top
            r2 = self.cn_rd_bottom
            z_offset = -abs_ht / 2.0
            
        bmesh.ops.create_cone(
            bm, 
            cap_ends=not self.cn_no_caps,
            cap_tris=True, 
            segments=48,
            radius1=r1, 
            radius2=r2, 
            depth=abs_ht
        )
        bmesh.ops.translate(bm, verts=bm.verts, vec=(0, 0, z_offset))
        
        if self.cn_flip_normals:
            bmesh.ops.reverse_faces(bm, faces=bm.faces)
        
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Cone_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Cone", make_cone, mat)
    obj.location = self.cn_loc
    obj.rotation_euler = self.cn_rot

def update_sphere(self, context):
    if not self.is_sp_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Sphere", self.sp_cl)
    
    def make_sphere():
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(
            bm, u_segments=32, v_segments=16, radius=self.sp_rd
        )
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Sphere_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Sphere", make_sphere, mat)
    obj.location = self.sp_loc
    obj.rotation_euler = self.sp_rot

def update_arrows(self, context):
    if not self.is_aw_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Arrows", self.aw_cl)
    
    def make_arrows():
        bm = bmesh.new()
        cnt = self.aw_cnt
        tg_loc = Vector(self.aw_tg_loc)
        
        tr_loc = Vector(self.tr_loc)
        tr_mat = Euler(self.tr_rot).to_matrix()
        tr_R = self.tr_mj_rd
        aw_thk = self.aw_thk
        pct = self.aw_offset_pct / 100.0  
        
        for i in range(cnt):
            theta = i * (2 * math.pi / cnt)
            local_p = Vector((tr_R * math.cos(theta), tr_R * math.sin(theta), 0))
            start_p = tr_loc + tr_mat @ local_p
            
            vec = tg_loc - start_p
            full_dist = vec.length
            if full_dist < 0.001: continue
                
            dist = full_dist * pct
            if dist < 0.001: continue
                
            dir_vec = vec.normalized()
            rot_quat = Vector((0,0,1)).rotation_difference(dir_vec)
            
            head_len = aw_thk * 4.0
            if head_len > dist * 0.5:
                head_len = dist * 0.5
            shaft_len = dist - head_len
            
            geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
                                         radius1=aw_thk, radius2=aw_thk, depth=shaft_len)
            verts_shaft = geom['verts']
            bmesh.ops.translate(bm, verts=verts_shaft, vec=(0,0,shaft_len/2))
            
            geom_h = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
                                           radius1=aw_thk*2.5, radius2=0, depth=head_len)
            verts_head = geom_h['verts']
            bmesh.ops.translate(bm, verts=verts_head, vec=(0,0,shaft_len + head_len/2))
            
            verts_all = verts_shaft + verts_head
            bmesh.ops.transform(bm, verts=verts_all, matrix=rot_quat.to_matrix().to_4x4())
            bmesh.ops.translate(bm, verts=verts_all, vec=start_p)
            
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Arrows_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Arrows", make_arrows, mat)
    obj.location = (0, 0, 0)
    obj.rotation_euler = (0, 0, 0)

def update_dome(self, context):
    if not self.is_dm_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Dome", self.dm_cl)
    
    def make_dome():
        crv = bpy.data.curves.new(f"{PREFIX_VAL}_Dome_Curve", 'CURVE')
        crv.dimensions = '3D'
        crv.fill_mode = 'FULL'
        crv.bevel_depth = self.dm_thk
        crv.bevel_resolution = 4
        
        rd = self.dm_rd
        ln_cn = self.dm_ln_cn
        lt_cn = self.dm_lt_cn
        sg = self.dm_sg
        
        # 経度のスプライン生成
        for i in range(ln_cn):
            theta = (2 * math.pi / ln_cn) * i
            spl = crv.splines.new('POLY')
            spl.points.add(sg - 1)
            for j in range(sg):
                phi = (math.pi / 2) * (j / (sg - 1))
                x = rd * math.sin(phi) * math.cos(theta)
                y = rd * math.sin(phi) * math.sin(theta)
                z = rd * math.cos(phi)
                spl.points[j].co = (x, y, z, 1.0)
                
        # 緯度のスプライン生成
        for i in range(1, lt_cn + 1):
            phi = (math.pi / 2) * (i / lt_cn)
            spl = crv.splines.new('POLY')
            spl.points.add(sg - 1)
            for j in range(sg):
                theta = (2 * math.pi) * (j / (sg - 1))
                x = rd * math.sin(phi) * math.cos(theta)
                y = rd * math.sin(phi) * math.sin(theta)
                z = rd * math.cos(phi)
                spl.points[j].co = (x, y, z, 1.0)
            spl.use_cyclic_u = True
            
        return crv

    obj = TCS_ARR_Utility.apply_curve(f"{PREFIX_VAL}_Dome", make_dome, mat)
    obj.location = self.dm_loc
    obj.rotation_euler = self.dm_rot

# =========================================================
# 8. プロパティグループ
# =========================================================
class TCS_ARR_PropertyGroup(bpy.types.PropertyGroup):
    # Torus
    is_tr_created: bpy.props.BoolProperty(default=False)
    tr_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,0,0), update=update_torus)
    tr_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_torus)
    tr_mj_rd: bpy.props.FloatProperty(name="Major Radius", default=3.0, min=0.1, update=update_torus)
    tr_mn_rd: bpy.props.FloatProperty(name="Minor Radius", default=0.2, min=0.01, update=update_torus)
    tr_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=update_torus)
    
    # Cone
    is_cn_created: bpy.props.BoolProperty(default=False)
    cn_loc: bpy.props.FloatVectorProperty(name="Location", default=(5,0,0), update=update_cone)
    cn_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_cone)
    
    cn_rd_top: bpy.props.FloatProperty(name="上面半径", default=0.0, min=0.0, update=update_cone)
    cn_rd_bottom: bpy.props.FloatProperty(name="下面半径", default=2.0, min=0.0, update=update_cone)
    cn_ht: bpy.props.FloatProperty(name="Height (マイナス許容)", default=4.0, update=update_cone)
    
    cn_no_caps: bpy.props.BoolProperty(name="蓋無し", default=False, update=update_cone)
    cn_flip_normals: bpy.props.BoolProperty(name="法線の反転 (裏表入替)", default=False, update=update_cone)
    
    cn_cl: bpy.props.FloatVectorProperty(name="表面色", subtype='COLOR', size=4, default=(1.0, 0.5, 0.1, 1.0), min=0, max=1, update=update_cone)
    cn_cl_back: bpy.props.FloatVectorProperty(name="裏面色", subtype='COLOR', size=4, default=(0.1, 0.5, 1.0, 1.0), min=0, max=1, update=update_cone)
    
    # Sphere
    is_sp_created: bpy.props.BoolProperty(default=False)
    sp_loc: bpy.props.FloatVectorProperty(name="Location", default=(-5,0,0), update=update_sphere)
    sp_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_sphere)
    sp_rd: bpy.props.FloatProperty(name="Radius", default=1.5, min=0.1, update=update_sphere)
    sp_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.9, 0.4, 1.0), min=0, max=1, update=update_sphere)
    
    # Arrows
    is_aw_created: bpy.props.BoolProperty(default=False)
    aw_cnt: bpy.props.IntProperty(name="矢印の本数", default=8, min=1, max=100, update=update_arrows)
    aw_tg_loc: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10.0), update=update_arrows)
    aw_offset_pct: bpy.props.FloatProperty(name="先端オフセット (%)", default=100.0, min=1.0, max=100.0, subtype='PERCENTAGE', update=update_arrows)
    aw_thk: bpy.props.FloatProperty(name="Thickness", default=0.1, min=0.01, update=update_arrows)
    aw_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.1, 0.1, 1.0), min=0, max=1, update=update_arrows)

    # Dome (Hemisphere Skeleton)
    is_dm_created: bpy.props.BoolProperty(default=False)
    dm_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,5,0), update=update_dome)
    dm_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_dome)
    dm_rd: bpy.props.FloatProperty(name="ドーム半径", default=3.0, min=0.1, update=update_dome)
    dm_thk: bpy.props.FloatProperty(name="フレーム太さ", default=0.1, min=0.01, update=update_dome)
    dm_ln_cn: bpy.props.IntProperty(name="経度の本数", default=12, min=3, max=100, update=update_dome)
    dm_lt_cn: bpy.props.IntProperty(name="緯度の本数", default=6, min=1, max=100, update=update_dome)
    dm_sg: bpy.props.IntProperty(name="分割数 (滑らかさ)", default=32, min=4, max=128, update=update_dome)
    dm_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.5, 0.3, 0.8, 1.0), min=0, max=1, update=update_dome)

# =========================================================
# 9. Operators
# =========================================================
class OT_create_torus(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_torus"
    bl_label = "トーラスを生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_tr_created = True
        update_torus(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_cone(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_cone"
    bl_label = "円錐台を生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_cn_created = True
        update_cone(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_sphere(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_sphere"
    bl_label = "球体を生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_sp_created = True
        update_sphere(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_arrows(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_arrows"
    bl_label = "矢印を生成"
    bl_description = "トーラスの円周からターゲット位置に向けて矢印を生成します"
    def execute(self, context):
        context.scene.tcs_arr_props.is_aw_created = True
        update_arrows(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_dome(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_dome"
    bl_label = "骨格ドームを生成"
    bl_description = "経度・緯度のカーブ構造を持つ半球ドームを生成します"
    def execute(self, context):
        context.scene.tcs_arr_props.is_dm_created = True
        update_dome(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_detach_torus(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_torus"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Torus", "Detached_Torus", "is_tr_created"):
            self.report({'INFO'}, "トーラスを独立させました")
        return {'FINISHED'}

class OT_detach_cone(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_cone"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Cone", "Detached_Cone", "is_cn_created"):
            self.report({'INFO'}, "円錐台を独立させました")
        return {'FINISHED'}

class OT_detach_sphere(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_sphere"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Sphere", "Detached_Sphere", "is_sp_created"):
            self.report({'INFO'}, "球体を独立させました")
        return {'FINISHED'}

class OT_detach_arrows(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_arrows"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Arrows", "Detached_Arrows", "is_aw_created"):
            self.report({'INFO'}, "矢印を独立させました")
        return {'FINISHED'}

class OT_detach_dome(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_dome"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Dome", "Detached_Dome", "is_dm_created"):
            self.report({'INFO'}, "骨格ドームを独立させました")
        return {'FINISHED'}

class OT_open_url(bpy.types.Operator):
    bl_idname = f"{PREFIX}.open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

def delayed_unregister():
    try:
        addon_utils.disable(__name__)
    except Exception:
        pass
    try:
        unregister()
    except Exception as e:
        print(f"[{ADDON_TITLE}] Delayed Unregister Error: {e}")
    return None

class OT_remove_addon(bpy.types.Operator):
    bl_idname = f"{PREFIX}.remove_addon"
    bl_label = "Clean up & Uninstall"
    bl_description = "生成したデータを全て消去し、アドオンを無効化してUIを閉じます"
    
    def execute(self, context):
        col_name = f"{PREFIX_VAL}_Collection"
        col = bpy.data.collections.get(col_name)
        if col:
            for obj in list(col.objects):
                bpy.data.objects.remove(obj, do_unlink=True)
            bpy.data.collections.remove(col)
            
        for mesh in list(bpy.data.meshes):
            if mesh.name.startswith(PREFIX_VAL):
                bpy.data.meshes.remove(mesh, do_unlink=True)
                
        # 追加: カーブデータもクリーンアップ
        for crv in list(bpy.data.curves):
            if crv.name.startswith(PREFIX_VAL):
                bpy.data.curves.remove(crv, do_unlink=True)
                
        for mat in list(bpy.data.materials):
            if mat.name.startswith(f"{PREFIX_VAL}_Mat"):
                bpy.data.materials.remove(mat, do_unlink=True)

        bpy.app.timers.register(delayed_unregister, first_interval=0.2)
        self.report({'INFO'}, "アドオンのデータをクリーンアップし、終了しました")
        return {'FINISHED'}

# =========================================================
# 10. パネル構成 (UI)
# =========================================================
class PT_Base(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

class PT_Torus(PT_Base):
    bl_label = "1. トーラス (Torus)"
    bl_idname = f"{PREFIX_VAL}_PT_torus"
    bl_order = 1
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "tr_loc")
        col.prop(props, "tr_rot")
        col.separator()
        col.prop(props, "tr_mj_rd")
        col.prop(props, "tr_mn_rd")
        col.prop(props, "tr_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_tr_created:
            row.operator(OT_create_torus.bl_idname, icon='MESH_TORUS')
        else:
            row.operator(OT_create_torus.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_torus.bl_idname, icon='UNLINKED')

class PT_Cone(PT_Base):
    bl_label = "2. 円錐台 (Cone)"
    bl_idname = f"{PREFIX_VAL}_PT_cone"
    bl_order = 2
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "cn_loc")
        col.prop(props, "cn_rot")
        col.separator()
        
        row = col.row(align=True)
        row.prop(props, "cn_rd_top")
        row.prop(props, "cn_rd_bottom")
        
        col.prop(props, "cn_ht")
        col.separator()
        
        row = col.row(align=True)
        row.prop(props, "cn_no_caps")
        row.prop(props, "cn_flip_normals")
        col.separator()

        row = col.row(align=True)
        row.prop(props, "cn_cl", text="表面")
        row.prop(props, "cn_cl_back", text="裏面")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_cn_created:
            row.operator(OT_create_cone.bl_idname, icon='MESH_CONE')
        else:
            row.operator(OT_create_cone.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_cone.bl_idname, icon='UNLINKED')

class PT_Sphere(PT_Base):
    bl_label = "3. 球体 (Sphere)"
    bl_idname = f"{PREFIX_VAL}_PT_sphere"
    bl_order = 3
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "sp_loc")
        col.prop(props, "sp_rot")
        col.separator()
        col.prop(props, "sp_rd")
        col.prop(props, "sp_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_sp_created:
            row.operator(OT_create_sphere.bl_idname, icon='MESH_UVSPHERE')
        else:
            row.operator(OT_create_sphere.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_sphere.bl_idname, icon='UNLINKED')

class PT_Arrows(PT_Base):
    bl_label = "4. 集中矢印 (Arrows)"
    bl_idname = f"{PREFIX_VAL}_PT_arrows"
    bl_order = 4
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        layout.label(text="※トーラス円周からターゲットへ向かいます", icon='INFO')
        col = layout.column(align=True)
        col.prop(props, "aw_cnt")
        col.prop(props, "aw_tg_loc")
        col.prop(props, "aw_offset_pct")
        col.separator()
        col.prop(props, "aw_thk")
        col.prop(props, "aw_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_aw_created:
            row.operator(OT_create_arrows.bl_idname, icon='TRACKING')
        else:
            row.operator(OT_create_arrows.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_arrows.bl_idname, icon='UNLINKED')

class PT_Dome(PT_Base):
    bl_label = "5. 骨格半球ドーム (Dome)"
    bl_idname = f"{PREFIX_VAL}_PT_dome"
    bl_order = 5
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "dm_loc")
        col.prop(props, "dm_rot")
        col.separator()
        
        col.prop(props, "dm_rd")
        col.prop(props, "dm_thk")
        col.separator()
        
        row = col.row(align=True)
        row.prop(props, "dm_ln_cn")
        row.prop(props, "dm_lt_cn")
        
        col.prop(props, "dm_sg")
        col.separator()
        
        col.prop(props, "dm_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_dm_created:
            row.operator(OT_create_dome.bl_idname, icon='MESH_UVSPHERE')
        else:
            row.operator(OT_create_dome.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_dome.bl_idname, icon='UNLINKED')

class PT_Links(PT_Base):
    bl_label = "Links"
    bl_idname = f"{PREFIX_VAL}_PT_links"
    bl_order = 10
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        if LINK_NAME_1: box.operator(OT_open_url.bl_idname, text=LINK_NAME_1, icon='URL').url = LINK_URL_1
        if LINK_NAME_2: box.operator(OT_open_url.bl_idname, text=LINK_NAME_2, icon='URL').url = LINK_URL_2
        if LINK_NAME_3: box.operator(OT_open_url.bl_idname, text=LINK_NAME_3, icon='URL').url = LINK_URL_3
        if LINK_NAME_4: box.operator(OT_open_url.bl_idname, text=LINK_NAME_4, icon='URL').url = LINK_URL_4
        if LINK_NAME_5: box.operator(OT_open_url.bl_idname, text=LINK_NAME_5, icon='URL').url = LINK_URL_5

class PT_Remove(PT_Base):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_remove"
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 11
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="生成物を全消去してアドオンを終了します", icon='ERROR')
        box.operator(OT_remove_addon.bl_idname, icon='CANCEL')

# =========================================================
# 11. 自動セットアップ
# =========================================================
def setup_workspace_on_activation():
    if not hasattr(bpy.context, 'window_manager') or not bpy.context.window_manager.windows:
        return 0.1
        
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            space.shading.type = 'MATERIAL'
                            space.show_region_ui = True
    except Exception:
        pass
        
    return None

# =========================================================
# 12. 登録と解除
# =========================================================
classes = [
    TCS_ARR_PropertyGroup,
    OT_create_torus, OT_create_cone, OT_create_sphere, OT_create_arrows, OT_create_dome,
    OT_detach_torus, OT_detach_cone, OT_detach_sphere, OT_detach_arrows, OT_detach_dome,
    OT_open_url, OT_remove_addon,
    PT_Torus, PT_Cone, PT_Sphere, PT_Arrows, PT_Dome, PT_Links, PT_Remove
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.tcs_arr_props = bpy.props.PointerProperty(type=TCS_ARR_PropertyGroup)
    
    if not bpy.app.timers.is_registered(setup_workspace_on_activation):
        bpy.app.timers.register(setup_workspace_on_activation, first_interval=0.1)

def unregister():
    try:
        del bpy.types.Scene.tcs_arr_props
    except Exception:
        pass
        
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass
            
    if bpy.app.timers.is_registered(setup_workspace_on_activation):
        bpy.app.timers.unregister(setup_workspace_on_activation)

if __name__ == "__main__":
    register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
    "name": "TCS & Arrows Generator",
    "author": "Your Name",
    "version": (2, 4, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar",
    "description": "トーラス・円錐台・球体・集中矢印(透明度完全対応・汎用円錐台・オフセット対応版)",
    "category": "3D View"
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1530

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import webbrowser
import addon_utils
from mathutils import Vector, Euler

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows"
PREFIX_VAL = "TCS_ARR"
ADDON_CATEGORY = "TCS Gen"
PREFIX = PREFIX_VAL.lower()

# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "マニュアルページ"
LINK_URL_1 = "<https://example.com/manual>"

LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"

LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

LINK_NAME_5 = ""
LINK_URL_5 = ""

# =========================================================
# 6. 共通ユーティリティ
# =========================================================
class TCS_ARR_Utility:
    @staticmethod
    def get_or_create_collection(name):
        col = bpy.data.collections.get(name)
        if not col:
            col = bpy.data.collections.new(name)
            bpy.context.scene.collection.children.link(col)
        return col

    @staticmethod
    def get_or_create_material(name, color):
        mat = bpy.data.materials.get(name)
        if not mat:
            mat = bpy.data.materials.new(name)
            mat.use_nodes = True
            
        # 透明度をマテリアルプレビューでも反映させるための設定
        try:
            if hasattr(mat, "blend_method"):
                mat.blend_method = 'BLEND'
            if hasattr(mat, "shadow_method"):
                mat.shadow_method = 'NONE'
        except Exception:
            pass

        if mat.use_nodes:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                if "Base Color" in bsdf.inputs:
                    bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)
                if "Alpha" in bsdf.inputs:
                    bsdf.inputs["Alpha"].default_value = color[3]
                    
        mat.diffuse_color = (color[0], color[1], color[2], color[3])
        return mat

    @staticmethod
    def apply_mesh(obj_name, mesh_func, mat):
        col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
        obj = bpy.data.objects.get(obj_name)
        if obj:
            old_mesh = obj.data
            obj.data = mesh_func()
            if old_mesh.users == 0:
                bpy.data.meshes.remove(old_mesh, do_unlink=True)
        else:
            mesh = mesh_func()
            obj = bpy.data.objects.new(obj_name, mesh)
            col.objects.link(obj)
            
        if len(obj.data.materials) == 0:
            obj.data.materials.append(mat)
        else:
            obj.data.materials[0] = mat
        return obj

    @staticmethod
    def detach_object(context, obj_name, base_new_name, flag_attr):
        obj = bpy.data.objects.get(obj_name)
        if not obj:
            return False
            
        ts_str = time.strftime("%H%M%S")
        new_name = f"{base_new_name}_{ts_str}"
        obj.name = new_name
        
        if obj.data:
            obj.data.name = f"{new_name}_Mesh"
            
        for slot in obj.material_slots:
            if slot.material and slot.material.name.startswith(PREFIX_VAL):
                new_mat = slot.material.copy()
                new_mat.name = f"{new_name}_Mat"
                slot.material = new_mat
                
        main_col = context.scene.collection
        if obj.name not in main_col.objects:
            main_col.objects.link(obj)
            
        addon_col = bpy.data.collections.get(f"{PREFIX_VAL}_Collection")
        if addon_col and obj.name in addon_col.objects:
            addon_col.objects.unlink(obj)
            
        setattr(context.scene.tcs_arr_props, flag_attr, False)
        return True

# =========================================================
# 7. 更新処理 (Update Functions)
# =========================================================
def update_torus(self, context):
    if not self.is_tr_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Torus", self.tr_cl)
    
    def make_torus():
        major_r = self.tr_mj_rd
        minor_r = self.tr_mn_rd
        segments, minor_segments = 48, 16
        verts, faces = [], []
        
        for i in range(segments):
            a1 = (i / segments) * 2.0 * math.pi
            c1, s1 = math.cos(a1), math.sin(a1)
            for j in range(minor_segments):
                a2 = (j / minor_segments) * 2.0 * math.pi
                c2, s2 = math.cos(a2), math.sin(a2)
                x = (major_r + minor_r * c2) * c1
                y = (major_r + minor_r * c2) * s1
                z = minor_r * s2
                verts.append((x, y, z))
                
        for i in range(segments):
            for j in range(minor_segments):
                i_next = (i + 1) % segments
                j_next = (j + 1) % minor_segments
                v0 = i * minor_segments + j
                v1 = i_next * minor_segments + j
                v2 = i_next * minor_segments + j_next
                v3 = i * minor_segments + j_next
                faces.append((v0, v1, v2, v3))
                
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Torus_Mesh")
        me.from_pydata(verts, [], faces)
        me.update()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Torus", make_torus, mat)
    obj.location = self.tr_loc
    obj.rotation_euler = self.tr_rot
    
    if self.is_aw_created:
        update_arrows(self, context)

def update_cone(self, context):
    if not self.is_cn_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Cone", self.cn_cl)
    
    def make_cone():
        bm = bmesh.new()
        ht = self.cn_ht
        abs_ht = abs(ht) if abs(ht) > 0.001 else 0.001
        
        # 高さの正負による制御
        # Z=0 の位置が常に「下面(Bottom)」の指定半径になるように調整
        if ht >= 0:
            r1 = self.cn_rd_bottom
            r2 = self.cn_rd_top
            z_offset = abs_ht / 2.0
        else:
            # 高さがマイナスの場合は下方向に伸びる(Z=0が下面のまま)
            r1 = self.cn_rd_top
            r2 = self.cn_rd_bottom
            z_offset = -abs_ht / 2.0
            
        bmesh.ops.create_cone(
            bm, 
            cap_ends=not self.cn_no_caps,  # 蓋の有無
            cap_tris=True, 
            segments=48,
            radius1=r1, 
            radius2=r2, 
            depth=abs_ht
        )
        
        # 指定した基準(Z=0)への移動
        bmesh.ops.translate(bm, verts=bm.verts, vec=(0, 0, z_offset))
        
        # 面の法線(裏表)の反転
        if self.cn_flip_normals:
            bmesh.ops.reverse_faces(bm, faces=bm.faces)
        
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Cone_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Cone", make_cone, mat)
    obj.location = self.cn_loc
    obj.rotation_euler = self.cn_rot

def update_sphere(self, context):
    if not self.is_sp_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Sphere", self.sp_cl)
    
    def make_sphere():
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(
            bm, u_segments=32, v_segments=16, radius=self.sp_rd
        )
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Sphere_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Sphere", make_sphere, mat)
    obj.location = self.sp_loc
    obj.rotation_euler = self.sp_rot

def update_arrows(self, context):
    if not self.is_aw_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Arrows", self.aw_cl)
    
    def make_arrows():
        bm = bmesh.new()
        cnt = self.aw_cnt
        tg_loc = Vector(self.aw_tg_loc)
        
        tr_loc = Vector(self.tr_loc)
        tr_mat = Euler(self.tr_rot).to_matrix()
        tr_R = self.tr_mj_rd
        aw_thk = self.aw_thk
        pct = self.aw_offset_pct / 100.0  
        
        for i in range(cnt):
            theta = i * (2 * math.pi / cnt)
            local_p = Vector((tr_R * math.cos(theta), tr_R * math.sin(theta), 0))
            start_p = tr_loc + tr_mat @ local_p
            
            vec = tg_loc - start_p
            full_dist = vec.length
            if full_dist < 0.001: continue
                
            dist = full_dist * pct
            if dist < 0.001: continue
                
            dir_vec = vec.normalized()
            rot_quat = Vector((0,0,1)).rotation_difference(dir_vec)
            
            head_len = aw_thk * 4.0
            if head_len > dist * 0.5:
                head_len = dist * 0.5
            shaft_len = dist - head_len
            
            geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
                                         radius1=aw_thk, radius2=aw_thk, depth=shaft_len)
            verts_shaft = geom['verts']
            bmesh.ops.translate(bm, verts=verts_shaft, vec=(0,0,shaft_len/2))
            
            geom_h = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
                                           radius1=aw_thk*2.5, radius2=0, depth=head_len)
            verts_head = geom_h['verts']
            bmesh.ops.translate(bm, verts=verts_head, vec=(0,0,shaft_len + head_len/2))
            
            verts_all = verts_shaft + verts_head
            bmesh.ops.transform(bm, verts=verts_all, matrix=rot_quat.to_matrix().to_4x4())
            bmesh.ops.translate(bm, verts=verts_all, vec=start_p)
            
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Arrows_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Arrows", make_arrows, mat)
    obj.location = (0, 0, 0)
    obj.rotation_euler = (0, 0, 0)

# =========================================================
# 8. プロパティグループ
# =========================================================
class TCS_ARR_PropertyGroup(bpy.types.PropertyGroup):
    # Torus
    is_tr_created: bpy.props.BoolProperty(default=False)
    tr_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,0,0), update=update_torus)
    tr_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_torus)
    tr_mj_rd: bpy.props.FloatProperty(name="Major Radius", default=3.0, min=0.1, update=update_torus)
    tr_mn_rd: bpy.props.FloatProperty(name="Minor Radius", default=0.2, min=0.01, update=update_torus)
    tr_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=update_torus)
    
    # Cone (円錐台対応)
    is_cn_created: bpy.props.BoolProperty(default=False)
    cn_loc: bpy.props.FloatVectorProperty(name="Location", default=(5,0,0), update=update_cone)
    cn_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_cone)
    
    cn_rd_top: bpy.props.FloatProperty(name="上面半径", default=0.0, min=0.0, update=update_cone)
    cn_rd_bottom: bpy.props.FloatProperty(name="下面半径", default=2.0, min=0.0, update=update_cone)
    cn_ht: bpy.props.FloatProperty(name="Height (マイナス許容)", default=4.0, update=update_cone)
    
    # 円錐の拡張設定
    cn_no_caps: bpy.props.BoolProperty(name="蓋無し", default=False, update=update_cone)
    cn_flip_normals: bpy.props.BoolProperty(name="法線の反転 (裏表)", default=False, update=update_cone)
    
    cn_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.5, 0.1, 1.0), min=0, max=1, update=update_cone)
    
    # Sphere
    is_sp_created: bpy.props.BoolProperty(default=False)
    sp_loc: bpy.props.FloatVectorProperty(name="Location", default=(-5,0,0), update=update_sphere)
    sp_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_sphere)
    sp_rd: bpy.props.FloatProperty(name="Radius", default=1.5, min=0.1, update=update_sphere)
    sp_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.9, 0.4, 1.0), min=0, max=1, update=update_sphere)
    
    # Arrows
    is_aw_created: bpy.props.BoolProperty(default=False)
    aw_cnt: bpy.props.IntProperty(name="矢印の本数", default=8, min=1, max=100, update=update_arrows)
    aw_tg_loc: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10.0), update=update_arrows)
    aw_offset_pct: bpy.props.FloatProperty(name="先端オフセット (%)", default=100.0, min=1.0, max=100.0, subtype='PERCENTAGE', update=update_arrows)
    aw_thk: bpy.props.FloatProperty(name="Thickness", default=0.1, min=0.01, update=update_arrows)
    aw_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.1, 0.1, 1.0), min=0, max=1, update=update_arrows)

# =========================================================
# 9. Operators
# =========================================================
class OT_create_torus(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_torus"
    bl_label = "トーラスを生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_tr_created = True
        update_torus(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_cone(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_cone"
    bl_label = "円錐台を生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_cn_created = True
        update_cone(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_sphere(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_sphere"
    bl_label = "球体を生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_sp_created = True
        update_sphere(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_arrows(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_arrows"
    bl_label = "矢印を生成"
    bl_description = "トーラスの円周からターゲット位置に向けて矢印を生成します"
    def execute(self, context):
        context.scene.tcs_arr_props.is_aw_created = True
        update_arrows(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_detach_torus(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_torus"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Torus", "Detached_Torus", "is_tr_created"):
            self.report({'INFO'}, "トーラスを独立させました")
        return {'FINISHED'}

class OT_detach_cone(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_cone"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Cone", "Detached_Cone", "is_cn_created"):
            self.report({'INFO'}, "円錐台を独立させました")
        return {'FINISHED'}

class OT_detach_sphere(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_sphere"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Sphere", "Detached_Sphere", "is_sp_created"):
            self.report({'INFO'}, "球体を独立させました")
        return {'FINISHED'}

class OT_detach_arrows(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_arrows"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Arrows", "Detached_Arrows", "is_aw_created"):
            self.report({'INFO'}, "矢印を独立させました")
        return {'FINISHED'}

class OT_open_url(bpy.types.Operator):
    bl_idname = f"{PREFIX}.open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

def delayed_unregister():
    try:
        addon_utils.disable(__name__)
    except Exception:
        pass
    try:
        unregister()
    except Exception as e:
        print(f"[{ADDON_TITLE}] Delayed Unregister Error: {e}")
    return None

class OT_remove_addon(bpy.types.Operator):
    bl_idname = f"{PREFIX}.remove_addon"
    bl_label = "Clean up & Uninstall"
    bl_description = "生成したデータを全て消去し、アドオンを無効化してUIを閉じます"
    
    def execute(self, context):
        col_name = f"{PREFIX_VAL}_Collection"
        col = bpy.data.collections.get(col_name)
        if col:
            for obj in list(col.objects):
                bpy.data.objects.remove(obj, do_unlink=True)
            bpy.data.collections.remove(col)
            
        for mesh in list(bpy.data.meshes):
            if mesh.name.startswith(PREFIX_VAL):
                bpy.data.meshes.remove(mesh, do_unlink=True)
                
        for mat in list(bpy.data.materials):
            if mat.name.startswith(f"{PREFIX_VAL}_Mat"):
                bpy.data.materials.remove(mat, do_unlink=True)

        bpy.app.timers.register(delayed_unregister, first_interval=0.2)
        self.report({'INFO'}, "アドオンのデータをクリーンアップし、終了しました")
        return {'FINISHED'}

# =========================================================
# 10. パネル構成 (UI)
# =========================================================
class PT_Base(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

class PT_Torus(PT_Base):
    bl_label = "1. トーラス (Torus)"
    bl_idname = f"{PREFIX_VAL}_PT_torus"
    bl_order = 1
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "tr_loc")
        col.prop(props, "tr_rot")
        col.separator()
        col.prop(props, "tr_mj_rd")
        col.prop(props, "tr_mn_rd")
        col.prop(props, "tr_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_tr_created:
            row.operator(OT_create_torus.bl_idname, icon='MESH_TORUS')
        else:
            row.operator(OT_create_torus.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_torus.bl_idname, icon='UNLINKED')

class PT_Cone(PT_Base):
    bl_label = "2. 円錐台 (Cone)"
    bl_idname = f"{PREFIX_VAL}_PT_cone"
    bl_order = 2
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "cn_loc")
        col.prop(props, "cn_rot")
        col.separator()
        
        # 円錐台用の上下半径設定
        row = col.row(align=True)
        row.prop(props, "cn_rd_top")
        row.prop(props, "cn_rd_bottom")
        
        col.prop(props, "cn_ht")
        col.separator()
        
        # 蓋の有無と裏表(法線)設定
        row = col.row(align=True)
        row.prop(props, "cn_no_caps")
        row.prop(props, "cn_flip_normals")
        col.separator()

        col.prop(props, "cn_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_cn_created:
            row.operator(OT_create_cone.bl_idname, icon='MESH_CONE')
        else:
            row.operator(OT_create_cone.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_cone.bl_idname, icon='UNLINKED')

class PT_Sphere(PT_Base):
    bl_label = "3. 球体 (Sphere)"
    bl_idname = f"{PREFIX_VAL}_PT_sphere"
    bl_order = 3
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "sp_loc")
        col.prop(props, "sp_rot")
        col.separator()
        col.prop(props, "sp_rd")
        col.prop(props, "sp_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_sp_created:
            row.operator(OT_create_sphere.bl_idname, icon='MESH_UVSPHERE')
        else:
            row.operator(OT_create_sphere.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_sphere.bl_idname, icon='UNLINKED')

class PT_Arrows(PT_Base):
    bl_label = "4. 集中矢印 (Arrows)"
    bl_idname = f"{PREFIX_VAL}_PT_arrows"
    bl_order = 4
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        layout.label(text="※トーラス円周からターゲットへ向かいます", icon='INFO')
        col = layout.column(align=True)
        col.prop(props, "aw_cnt")
        col.prop(props, "aw_tg_loc")
        col.prop(props, "aw_offset_pct")
        col.separator()
        col.prop(props, "aw_thk")
        col.prop(props, "aw_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_aw_created:
            row.operator(OT_create_arrows.bl_idname, icon='TRACKING')
        else:
            row.operator(OT_create_arrows.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_arrows.bl_idname, icon='UNLINKED')

class PT_Links(PT_Base):
    bl_label = "Links"
    bl_idname = f"{PREFIX_VAL}_PT_links"
    bl_order = 10
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        if LINK_NAME_1: box.operator(OT_open_url.bl_idname, text=LINK_NAME_1, icon='URL').url = LINK_URL_1
        if LINK_NAME_2: box.operator(OT_open_url.bl_idname, text=LINK_NAME_2, icon='URL').url = LINK_URL_2
        if LINK_NAME_3: box.operator(OT_open_url.bl_idname, text=LINK_NAME_3, icon='URL').url = LINK_URL_3
        if LINK_NAME_4: box.operator(OT_open_url.bl_idname, text=LINK_NAME_4, icon='URL').url = LINK_URL_4
        if LINK_NAME_5: box.operator(OT_open_url.bl_idname, text=LINK_NAME_5, icon='URL').url = LINK_URL_5

class PT_Remove(PT_Base):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_remove"
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 11
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="生成物を全消去してアドオンを終了します", icon='ERROR')
        box.operator(OT_remove_addon.bl_idname, icon='CANCEL')

# =========================================================
# 11. 自動セットアップ
# =========================================================
def setup_workspace_on_activation():
    if not hasattr(bpy.context, 'window_manager') or not bpy.context.window_manager.windows:
        return 0.1
        
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            space.shading.type = 'MATERIAL'
                            space.show_region_ui = True
    except Exception:
        pass
        
    return None

# =========================================================
# 12. 登録と解除
# =========================================================
classes = [
    TCS_ARR_PropertyGroup,
    OT_create_torus, OT_create_cone, OT_create_sphere, OT_create_arrows,
    OT_detach_torus, OT_detach_cone, OT_detach_sphere, OT_detach_arrows,
    OT_open_url, OT_remove_addon,
    PT_Torus, PT_Cone, PT_Sphere, PT_Arrows, PT_Links, PT_Remove
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.tcs_arr_props = bpy.props.PointerProperty(type=TCS_ARR_PropertyGroup)
    
    if not bpy.app.timers.is_registered(setup_workspace_on_activation):
        bpy.app.timers.register(setup_workspace_on_activation, first_interval=0.1)

def unregister():
    try:
        del bpy.types.Scene.tcs_arr_props
    except Exception:
        pass
        
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass
            
    if bpy.app.timers.is_registered(setup_workspace_on_activation):
        bpy.app.timers.unregister(setup_workspace_on_activation)

if __name__ == "__main__":
    register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
    "name": "TCS & Arrows Generator",
    "author": "Your Name",
    "version": (2, 3, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar",
    "description": "トーラス・円錐・球体・集中矢印(透明度完全対応・逆さ円錐・オフセット対応版)",
    "category": "3D View"
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1530

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import webbrowser
import addon_utils
from mathutils import Vector, Euler

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows"
PREFIX_VAL = "TCS_ARR"
ADDON_CATEGORY = "TCS Gen"
PREFIX = PREFIX_VAL.lower()

# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "マニュアルページ"
LINK_URL_1 = "<https://example.com/manual>"

LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"

LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

LINK_NAME_5 = ""
LINK_URL_5 = ""

# =========================================================
# 6. 共通ユーティリティ
# =========================================================
class TCS_ARR_Utility:
    @staticmethod
    def get_or_create_collection(name):
        col = bpy.data.collections.get(name)
        if not col:
            col = bpy.data.collections.new(name)
            bpy.context.scene.collection.children.link(col)
        return col

    @staticmethod
    def get_or_create_material(name, color):
        mat = bpy.data.materials.get(name)
        if not mat:
            mat = bpy.data.materials.new(name)
            mat.use_nodes = True
            
        # 透明度をマテリアルプレビューでも反映させるための設定
        try:
            if hasattr(mat, "blend_method"):
                mat.blend_method = 'BLEND'
            if hasattr(mat, "shadow_method"):
                mat.shadow_method = 'NONE'
        except Exception:
            pass

        if mat.use_nodes:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                # Base ColorにはRGBを渡し、Alphaには透明度を個別に渡す(透過不良の原因を排除)
                if "Base Color" in bsdf.inputs:
                    bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)
                if "Alpha" in bsdf.inputs:
                    bsdf.inputs["Alpha"].default_value = color[3]
                    
        # ビューポート(Solid)のカラーにもAlphaを反映
        mat.diffuse_color = (color[0], color[1], color[2], color[3])
        return mat

    @staticmethod
    def apply_mesh(obj_name, mesh_func, mat):
        col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
        obj = bpy.data.objects.get(obj_name)
        if obj:
            old_mesh = obj.data
            obj.data = mesh_func()
            if old_mesh.users == 0:
                bpy.data.meshes.remove(old_mesh, do_unlink=True)
        else:
            mesh = mesh_func()
            obj = bpy.data.objects.new(obj_name, mesh)
            col.objects.link(obj)
            
        if len(obj.data.materials) == 0:
            obj.data.materials.append(mat)
        else:
            obj.data.materials[0] = mat
        return obj

    @staticmethod
    def detach_object(context, obj_name, base_new_name, flag_attr):
        """アドオンの管理下からオブジェクトを切り離し、独立させる"""
        obj = bpy.data.objects.get(obj_name)
        if not obj:
            return False
            
        ts_str = time.strftime("%H%M%S")
        new_name = f"{base_new_name}_{ts_str}"
        obj.name = new_name
        
        if obj.data:
            obj.data.name = f"{new_name}_Mesh"
            
        for slot in obj.material_slots:
            if slot.material and slot.material.name.startswith(PREFIX_VAL):
                new_mat = slot.material.copy()
                new_mat.name = f"{new_name}_Mat"
                slot.material = new_mat
                
        main_col = context.scene.collection
        if obj.name not in main_col.objects:
            main_col.objects.link(obj)
            
        addon_col = bpy.data.collections.get(f"{PREFIX_VAL}_Collection")
        if addon_col and obj.name in addon_col.objects:
            addon_col.objects.unlink(obj)
            
        setattr(context.scene.tcs_arr_props, flag_attr, False)
        return True

# =========================================================
# 7. 更新処理 (Update Functions)
# =========================================================
def update_torus(self, context):
    if not self.is_tr_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Torus", self.tr_cl)
    
    def make_torus():
        major_r = self.tr_mj_rd
        minor_r = self.tr_mn_rd
        segments, minor_segments = 48, 16
        verts, faces = [], []
        
        for i in range(segments):
            a1 = (i / segments) * 2.0 * math.pi
            c1, s1 = math.cos(a1), math.sin(a1)
            for j in range(minor_segments):
                a2 = (j / minor_segments) * 2.0 * math.pi
                c2, s2 = math.cos(a2), math.sin(a2)
                x = (major_r + minor_r * c2) * c1
                y = (major_r + minor_r * c2) * s1
                z = minor_r * s2
                verts.append((x, y, z))
                
        for i in range(segments):
            for j in range(minor_segments):
                i_next = (i + 1) % segments
                j_next = (j + 1) % minor_segments
                v0 = i * minor_segments + j
                v1 = i_next * minor_segments + j
                v2 = i_next * minor_segments + j_next
                v3 = i * minor_segments + j_next
                faces.append((v0, v1, v2, v3))
                
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Torus_Mesh")
        me.from_pydata(verts, [], faces)
        me.update()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Torus", make_torus, mat)
    obj.location = self.tr_loc
    obj.rotation_euler = self.tr_rot
    
    # トーラスが更新されたら矢印も追従して更新する
    if self.is_aw_created:
        update_arrows(self, context)

def update_cone(self, context):
    if not self.is_cn_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Cone", self.cn_cl)
    
    def make_cone():
        bm = bmesh.new()
        ht = self.cn_ht
        abs_ht = abs(ht) if abs(ht) > 0.001 else 0.001
        
        # マイナス値の場合は逆さ円錐(下に向かって尖る)にする
        if ht >= 0:
            r1, r2 = self.cn_rd, 0
            z_offset = abs_ht / 2.0
        else:
            r1, r2 = 0, self.cn_rd
            z_offset = -abs_ht / 2.0
            
        bmesh.ops.create_cone(
            bm, cap_ends=True, cap_tris=True, segments=48,
            radius1=r1, radius2=r2, depth=abs_ht
        )
        # Z=0 の位置が常に底面になるように移動
        bmesh.ops.translate(bm, verts=bm.verts, vec=(0, 0, z_offset))
        
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Cone_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Cone", make_cone, mat)
    obj.location = self.cn_loc
    obj.rotation_euler = self.cn_rot

def update_sphere(self, context):
    if not self.is_sp_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Sphere", self.sp_cl)
    
    def make_sphere():
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(
            bm, u_segments=32, v_segments=16, radius=self.sp_rd
        )
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Sphere_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Sphere", make_sphere, mat)
    obj.location = self.sp_loc
    obj.rotation_euler = self.sp_rot

def update_arrows(self, context):
    if not self.is_aw_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Arrows", self.aw_cl)
    
    def make_arrows():
        bm = bmesh.new()
        cnt = self.aw_cnt
        tg_loc = Vector(self.aw_tg_loc)
        
        # トーラスの位置・回転・半径を取得して発生源とする
        tr_loc = Vector(self.tr_loc)
        tr_mat = Euler(self.tr_rot).to_matrix()
        tr_R = self.tr_mj_rd
        aw_thk = self.aw_thk
        pct = self.aw_offset_pct / 100.0  # オフセット割合(%)
        
        for i in range(cnt):
            theta = i * (2 * math.pi / cnt)
            local_p = Vector((tr_R * math.cos(theta), tr_R * math.sin(theta), 0))
            start_p = tr_loc + tr_mat @ local_p
            
            vec = tg_loc - start_p
            full_dist = vec.length
            if full_dist < 0.001:
                continue
                
            # 割合(オフセット)を反映した長さを計算
            dist = full_dist * pct
            if dist < 0.001:
                continue
                
            dir_vec = vec.normalized()
            rot_quat = Vector((0,0,1)).rotation_difference(dir_vec)
            
            head_len = aw_thk * 4.0
            if head_len > dist * 0.5:
                head_len = dist * 0.5
            shaft_len = dist - head_len
            
            # 軸
            geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
                                         radius1=aw_thk, radius2=aw_thk, depth=shaft_len)
            verts_shaft = geom['verts']
            bmesh.ops.translate(bm, verts=verts_shaft, vec=(0,0,shaft_len/2))
            
            # 矢尻
            geom_h = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
                                           radius1=aw_thk*2.5, radius2=0, depth=head_len)
            verts_head = geom_h['verts']
            bmesh.ops.translate(bm, verts=verts_head, vec=(0,0,shaft_len + head_len/2))
            
            verts_all = verts_shaft + verts_head
            bmesh.ops.transform(bm, verts=verts_all, matrix=rot_quat.to_matrix().to_4x4())
            bmesh.ops.translate(bm, verts=verts_all, vec=start_p)
            
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Arrows_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Arrows", make_arrows, mat)
    obj.location = (0, 0, 0)
    obj.rotation_euler = (0, 0, 0)

# =========================================================
# 8. プロパティグループ
# =========================================================
class TCS_ARR_PropertyGroup(bpy.types.PropertyGroup):
    # Torus
    is_tr_created: bpy.props.BoolProperty(default=False)
    tr_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,0,0), update=update_torus)
    tr_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_torus)
    tr_mj_rd: bpy.props.FloatProperty(name="Major Radius", default=3.0, min=0.1, update=update_torus)
    tr_mn_rd: bpy.props.FloatProperty(name="Minor Radius", default=0.2, min=0.01, update=update_torus)
    tr_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=update_torus)
    
    # Cone
    is_cn_created: bpy.props.BoolProperty(default=False)
    cn_loc: bpy.props.FloatVectorProperty(name="Location", default=(5,0,0), update=update_cone)
    cn_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_cone)
    cn_rd: bpy.props.FloatProperty(name="Radius", default=2.0, min=0.1, update=update_cone)
    cn_ht: bpy.props.FloatProperty(name="Height (マイナス許容)", default=4.0, update=update_cone)  # min制約を撤廃
    cn_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.5, 0.1, 1.0), min=0, max=1, update=update_cone)
    
    # Sphere
    is_sp_created: bpy.props.BoolProperty(default=False)
    sp_loc: bpy.props.FloatVectorProperty(name="Location", default=(-5,0,0), update=update_sphere)
    sp_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_sphere)
    sp_rd: bpy.props.FloatProperty(name="Radius", default=1.5, min=0.1, update=update_sphere)
    sp_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.9, 0.4, 1.0), min=0, max=1, update=update_sphere)
    
    # Arrows
    is_aw_created: bpy.props.BoolProperty(default=False)
    aw_cnt: bpy.props.IntProperty(name="矢印の本数", default=8, min=1, max=100, update=update_arrows)
    aw_tg_loc: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10.0), update=update_arrows)
    aw_offset_pct: bpy.props.FloatProperty(name="先端オフセット (%)", default=100.0, min=1.0, max=100.0, subtype='PERCENTAGE', update=update_arrows)
    aw_thk: bpy.props.FloatProperty(name="Thickness", default=0.1, min=0.01, update=update_arrows)
    aw_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.1, 0.1, 1.0), min=0, max=1, update=update_arrows)

# =========================================================
# 9. Operators
# =========================================================
class OT_create_torus(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_torus"
    bl_label = "トーラスを生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_tr_created = True
        update_torus(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_cone(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_cone"
    bl_label = "円錐を生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_cn_created = True
        update_cone(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_sphere(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_sphere"
    bl_label = "球体を生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_sp_created = True
        update_sphere(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_arrows(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_arrows"
    bl_label = "矢印を生成"
    bl_description = "トーラスの円周からターゲット位置に向けて矢印を生成します"
    def execute(self, context):
        context.scene.tcs_arr_props.is_aw_created = True
        update_arrows(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_detach_torus(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_torus"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Torus", "Detached_Torus", "is_tr_created"):
            self.report({'INFO'}, "トーラスを独立させました")
        return {'FINISHED'}

class OT_detach_cone(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_cone"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Cone", "Detached_Cone", "is_cn_created"):
            self.report({'INFO'}, "円錐を独立させました")
        return {'FINISHED'}

class OT_detach_sphere(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_sphere"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Sphere", "Detached_Sphere", "is_sp_created"):
            self.report({'INFO'}, "球体を独立させました")
        return {'FINISHED'}

class OT_detach_arrows(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_arrows"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Arrows", "Detached_Arrows", "is_aw_created"):
            self.report({'INFO'}, "矢印を独立させました")
        return {'FINISHED'}

class OT_open_url(bpy.types.Operator):
    bl_idname = f"{PREFIX}.open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

def delayed_unregister():
    try:
        addon_utils.disable(__name__)
    except Exception:
        pass
    try:
        unregister()
    except Exception as e:
        print(f"[{ADDON_TITLE}] Delayed Unregister Error: {e}")
    return None

class OT_remove_addon(bpy.types.Operator):
    bl_idname = f"{PREFIX}.remove_addon"
    bl_label = "Clean up & Uninstall"
    bl_description = "生成したデータを全て消去し、アドオンを無効化してUIを閉じます"
    
    def execute(self, context):
        col_name = f"{PREFIX_VAL}_Collection"
        col = bpy.data.collections.get(col_name)
        if col:
            for obj in list(col.objects):
                bpy.data.objects.remove(obj, do_unlink=True)
            bpy.data.collections.remove(col)
            
        for mesh in list(bpy.data.meshes):
            if mesh.name.startswith(PREFIX_VAL):
                bpy.data.meshes.remove(mesh, do_unlink=True)
                
        for mat in list(bpy.data.materials):
            if mat.name.startswith(f"{PREFIX_VAL}_Mat"):
                bpy.data.materials.remove(mat, do_unlink=True)

        bpy.app.timers.register(delayed_unregister, first_interval=0.2)
        self.report({'INFO'}, "アドオンのデータをクリーンアップし、終了しました")
        return {'FINISHED'}

# =========================================================
# 10. パネル構成 (UI)
# =========================================================
class PT_Base(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

class PT_Torus(PT_Base):
    bl_label = "1. トーラス (Torus)"
    bl_idname = f"{PREFIX_VAL}_PT_torus"
    bl_order = 1
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "tr_loc")
        col.prop(props, "tr_rot")
        col.separator()
        col.prop(props, "tr_mj_rd")
        col.prop(props, "tr_mn_rd")
        col.prop(props, "tr_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_tr_created:
            row.operator(OT_create_torus.bl_idname, icon='MESH_TORUS')
        else:
            row.operator(OT_create_torus.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_torus.bl_idname, icon='UNLINKED')

class PT_Cone(PT_Base):
    bl_label = "2. 円錐 (Cone)"
    bl_idname = f"{PREFIX_VAL}_PT_cone"
    bl_order = 2
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "cn_loc")
        col.prop(props, "cn_rot")
        col.separator()
        col.prop(props, "cn_rd")
        col.prop(props, "cn_ht")
        col.prop(props, "cn_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_cn_created:
            row.operator(OT_create_cone.bl_idname, icon='MESH_CONE')
        else:
            row.operator(OT_create_cone.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_cone.bl_idname, icon='UNLINKED')

class PT_Sphere(PT_Base):
    bl_label = "3. 球体 (Sphere)"
    bl_idname = f"{PREFIX_VAL}_PT_sphere"
    bl_order = 3
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "sp_loc")
        col.prop(props, "sp_rot")
        col.separator()
        col.prop(props, "sp_rd")
        col.prop(props, "sp_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_sp_created:
            row.operator(OT_create_sphere.bl_idname, icon='MESH_UVSPHERE')
        else:
            row.operator(OT_create_sphere.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_sphere.bl_idname, icon='UNLINKED')

class PT_Arrows(PT_Base):
    bl_label = "4. 集中矢印 (Arrows)"
    bl_idname = f"{PREFIX_VAL}_PT_arrows"
    bl_order = 4
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        layout.label(text="※トーラス円周からターゲットへ向かいます", icon='INFO')
        col = layout.column(align=True)
        col.prop(props, "aw_cnt")
        col.prop(props, "aw_tg_loc")
        col.prop(props, "aw_offset_pct")
        col.separator()
        col.prop(props, "aw_thk")
        col.prop(props, "aw_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_aw_created:
            row.operator(OT_create_arrows.bl_idname, icon='TRACKING')
        else:
            row.operator(OT_create_arrows.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_arrows.bl_idname, icon='UNLINKED')

class PT_Links(PT_Base):
    bl_label = "Links"
    bl_idname = f"{PREFIX_VAL}_PT_links"
    bl_order = 10
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        if LINK_NAME_1: box.operator(OT_open_url.bl_idname, text=LINK_NAME_1, icon='URL').url = LINK_URL_1
        if LINK_NAME_2: box.operator(OT_open_url.bl_idname, text=LINK_NAME_2, icon='URL').url = LINK_URL_2
        if LINK_NAME_3: box.operator(OT_open_url.bl_idname, text=LINK_NAME_3, icon='URL').url = LINK_URL_3
        if LINK_NAME_4: box.operator(OT_open_url.bl_idname, text=LINK_NAME_4, icon='URL').url = LINK_URL_4
        if LINK_NAME_5: box.operator(OT_open_url.bl_idname, text=LINK_NAME_5, icon='URL').url = LINK_URL_5

class PT_Remove(PT_Base):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_remove"
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 11
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="生成物を全消去してアドオンを終了します", icon='ERROR')
        box.operator(OT_remove_addon.bl_idname, icon='CANCEL')

# =========================================================
# 11. 自動セットアップ
# =========================================================
def setup_workspace_on_activation():
    if not hasattr(bpy.context, 'window_manager') or not bpy.context.window_manager.windows:
        return 0.1
        
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            space.shading.type = 'MATERIAL'
                            space.show_region_ui = True
    except Exception:
        pass
        
    return None

# =========================================================
# 12. 登録と解除
# =========================================================
classes = [
    TCS_ARR_PropertyGroup,
    OT_create_torus, OT_create_cone, OT_create_sphere, OT_create_arrows,
    OT_detach_torus, OT_detach_cone, OT_detach_sphere, OT_detach_arrows,
    OT_open_url, OT_remove_addon,
    PT_Torus, PT_Cone, PT_Sphere, PT_Arrows, PT_Links, PT_Remove
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.tcs_arr_props = bpy.props.PointerProperty(type=TCS_ARR_PropertyGroup)
    
    if not bpy.app.timers.is_registered(setup_workspace_on_activation):
        bpy.app.timers.register(setup_workspace_on_activation, first_interval=0.1)

def unregister():
    try:
        del bpy.types.Scene.tcs_arr_props
    except Exception:
        pass
        
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass
            
    if bpy.app.timers.is_registered(setup_workspace_on_activation):
        bpy.app.timers.unregister(setup_workspace_on_activation)

if __name__ == "__main__":
    register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
    "name": "TCS & Arrows Generator",
    "author": "Your Name",
    "version": (2, 1, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar",
    "description": "トーラス・円錐・球体・集中矢印を個別に生成・管理・自己クリーンアップ可能",
    "category": "3D View"
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1430

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import webbrowser
import addon_utils
from mathutils import Vector, Euler

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows"
PREFIX_VAL = "TCS_ARR"
ADDON_CATEGORY = "TCS Gen"
PREFIX = PREFIX_VAL.lower()

# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "マニュアルページ"
LINK_URL_1 = "<https://example.com/manual>"

LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"

LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

LINK_NAME_5 = ""
LINK_URL_5 = ""

# =========================================================
# 6. 共通ユーティリティ
# =========================================================
class TCS_ARR_Utility:
    @staticmethod
    def get_or_create_collection(name):
        col = bpy.data.collections.get(name)
        if not col:
            col = bpy.data.collections.new(name)
            bpy.context.scene.collection.children.link(col)
        return col

    @staticmethod
    def get_or_create_material(name, color):
        mat = bpy.data.materials.get(name)
        if not mat:
            mat = bpy.data.materials.new(name)
            mat.use_nodes = True
        if mat.use_nodes:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                bsdf.inputs["Base Color"].default_value = color
        return mat

    @staticmethod
    def apply_mesh(obj_name, mesh_func, mat):
        col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
        obj = bpy.data.objects.get(obj_name)
        if obj:
            old_mesh = obj.data
            obj.data = mesh_func()
            if old_mesh.users == 0:
                bpy.data.meshes.remove(old_mesh, do_unlink=True)
        else:
            mesh = mesh_func()
            obj = bpy.data.objects.new(obj_name, mesh)
            col.objects.link(obj)
            
        if len(obj.data.materials) == 0:
            obj.data.materials.append(mat)
        else:
            obj.data.materials[0] = mat
        return obj

    @staticmethod
    def detach_object(context, obj_name, base_new_name, flag_attr):
        """アドオンの管理下からオブジェクトを切り離し、独立させる"""
        obj = bpy.data.objects.get(obj_name)
        if not obj:
            return False
            
        ts_str = time.strftime("%H%M%S")
        new_name = f"{base_new_name}_{ts_str}"
        obj.name = new_name
        
        if obj.data:
            obj.data.name = f"{new_name}_Mesh"
            
        for slot in obj.material_slots:
            if slot.material and slot.material.name.startswith(PREFIX_VAL):
                new_mat = slot.material.copy()
                new_mat.name = f"{new_name}_Mat"
                slot.material = new_mat
                
        main_col = context.scene.collection
        if obj.name not in main_col.objects:
            main_col.objects.link(obj)
            
        addon_col = bpy.data.collections.get(f"{PREFIX_VAL}_Collection")
        if addon_col and obj.name in addon_col.objects:
            addon_col.objects.unlink(obj)
            
        setattr(context.scene.tcs_arr_props, flag_attr, False)
        return True

# =========================================================
# 7. 更新処理 (Update Functions)
# =========================================================
def update_torus(self, context):
    if not self.is_tr_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Torus", self.tr_cl)
    
    def make_torus():
        major_r = self.tr_mj_rd
        minor_r = self.tr_mn_rd
        segments, minor_segments = 48, 16
        verts, faces = [], []
        
        for i in range(segments):
            a1 = (i / segments) * 2.0 * math.pi
            c1, s1 = math.cos(a1), math.sin(a1)
            for j in range(minor_segments):
                a2 = (j / minor_segments) * 2.0 * math.pi
                c2, s2 = math.cos(a2), math.sin(a2)
                x = (major_r + minor_r * c2) * c1
                y = (major_r + minor_r * c2) * s1
                z = minor_r * s2
                verts.append((x, y, z))
                
        for i in range(segments):
            for j in range(minor_segments):
                i_next = (i + 1) % segments
                j_next = (j + 1) % minor_segments
                v0 = i * minor_segments + j
                v1 = i_next * minor_segments + j
                v2 = i_next * minor_segments + j_next
                v3 = i * minor_segments + j_next
                faces.append((v0, v1, v2, v3))
                
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Torus_Mesh")
        me.from_pydata(verts, [], faces)
        me.update()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Torus", make_torus, mat)
    obj.location = self.tr_loc
    obj.rotation_euler = self.tr_rot
    
    # トーラスが更新されたら矢印も追従して更新する
    if self.is_aw_created:
        update_arrows(self, context)

def update_cone(self, context):
    if not self.is_cn_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Cone", self.cn_cl)
    
    def make_cone():
        bm = bmesh.new()
        bmesh.ops.create_cone(
            bm, cap_ends=True, cap_tris=True, segments=48,
            radius1=self.cn_rd, radius2=0, depth=self.cn_ht
        )
        bmesh.ops.translate(bm, verts=bm.verts, vec=(0, 0, self.cn_ht / 2))
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Cone_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Cone", make_cone, mat)
    obj.location = self.cn_loc
    obj.rotation_euler = self.cn_rot

def update_sphere(self, context):
    if not self.is_sp_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Sphere", self.sp_cl)
    
    def make_sphere():
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(
            bm, u_segments=32, v_segments=16, radius=self.sp_rd
        )
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Sphere_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Sphere", make_sphere, mat)
    obj.location = self.sp_loc
    obj.rotation_euler = self.sp_rot

def update_arrows(self, context):
    if not self.is_aw_created: return
    mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Arrows", self.aw_cl)
    
    def make_arrows():
        bm = bmesh.new()
        cnt = self.aw_cnt
        tg_loc = Vector(self.aw_tg_loc)
        
        # トーラスの位置・回転・半径を取得して発生源とする
        tr_loc = Vector(self.tr_loc)
        tr_mat = Euler(self.tr_rot).to_matrix()
        tr_R = self.tr_mj_rd
        aw_thk = self.aw_thk
        
        for i in range(cnt):
            theta = i * (2 * math.pi / cnt)
            local_p = Vector((tr_R * math.cos(theta), tr_R * math.sin(theta), 0))
            start_p = tr_loc + tr_mat @ local_p
            
            vec = tg_loc - start_p
            dist = vec.length
            if dist < 0.001:
                continue
                
            dir_vec = vec.normalized()
            rot_quat = Vector((0,0,1)).rotation_difference(dir_vec)
            
            head_len = aw_thk * 4.0
            if head_len > dist * 0.5:
                head_len = dist * 0.5
            shaft_len = dist - head_len
            
            # 軸
            geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
                                         radius1=aw_thk, radius2=aw_thk, depth=shaft_len)
            verts_shaft = geom['verts']
            bmesh.ops.translate(bm, verts=verts_shaft, vec=(0,0,shaft_len/2))
            
            # 矢尻
            geom_h = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
                                           radius1=aw_thk*2.5, radius2=0, depth=head_len)
            verts_head = geom_h['verts']
            bmesh.ops.translate(bm, verts=verts_head, vec=(0,0,shaft_len + head_len/2))
            
            verts_all = verts_shaft + verts_head
            bmesh.ops.transform(bm, verts=verts_all, matrix=rot_quat.to_matrix().to_4x4())
            bmesh.ops.translate(bm, verts=verts_all, vec=start_p)
            
        me = bpy.data.meshes.new(f"{PREFIX_VAL}_Arrows_Mesh")
        bm.to_mesh(me)
        bm.free()
        return me

    obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Arrows", make_arrows, mat)
    obj.location = (0, 0, 0)
    obj.rotation_euler = (0, 0, 0)

# =========================================================
# 8. プロパティグループ
# =========================================================
class TCS_ARR_PropertyGroup(bpy.types.PropertyGroup):
    # Torus
    is_tr_created: bpy.props.BoolProperty(default=False)
    tr_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,0,0), update=update_torus)
    tr_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_torus)
    tr_mj_rd: bpy.props.FloatProperty(name="Major Radius", default=3.0, min=0.1, update=update_torus)
    tr_mn_rd: bpy.props.FloatProperty(name="Minor Radius", default=0.2, min=0.01, update=update_torus)
    tr_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=update_torus)
    
    # Cone
    is_cn_created: bpy.props.BoolProperty(default=False)
    cn_loc: bpy.props.FloatVectorProperty(name="Location", default=(5,0,0), update=update_cone)
    cn_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_cone)
    cn_rd: bpy.props.FloatProperty(name="Radius", default=2.0, min=0.1, update=update_cone)
    cn_ht: bpy.props.FloatProperty(name="Height", default=4.0, min=0.1, update=update_cone)
    cn_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.5, 0.1, 1.0), min=0, max=1, update=update_cone)
    
    # Sphere
    is_sp_created: bpy.props.BoolProperty(default=False)
    sp_loc: bpy.props.FloatVectorProperty(name="Location", default=(-5,0,0), update=update_sphere)
    sp_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_sphere)
    sp_rd: bpy.props.FloatProperty(name="Radius", default=1.5, min=0.1, update=update_sphere)
    sp_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.9, 0.4, 1.0), min=0, max=1, update=update_sphere)
    
    # Arrows
    is_aw_created: bpy.props.BoolProperty(default=False)
    aw_cnt: bpy.props.IntProperty(name="矢印の本数", default=8, min=1, max=100, update=update_arrows)
    aw_tg_loc: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10.0), update=update_arrows)
    aw_thk: bpy.props.FloatProperty(name="Thickness", default=0.1, min=0.01, update=update_arrows)
    aw_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.1, 0.1, 1.0), min=0, max=1, update=update_arrows)

# =========================================================
# 9. Operators
# =========================================================
class OT_create_torus(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_torus"
    bl_label = "トーラスを生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_tr_created = True
        update_torus(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_cone(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_cone"
    bl_label = "円錐を生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_cn_created = True
        update_cone(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_sphere(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_sphere"
    bl_label = "球体を生成"
    def execute(self, context):
        context.scene.tcs_arr_props.is_sp_created = True
        update_sphere(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_create_arrows(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_arrows"
    bl_label = "矢印を生成"
    bl_description = "トーラスの円周からターゲット位置に向けて矢印を生成します"
    def execute(self, context):
        context.scene.tcs_arr_props.is_aw_created = True
        update_arrows(context.scene.tcs_arr_props, context)
        return {'FINISHED'}

class OT_detach_torus(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_torus"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Torus", "Detached_Torus", "is_tr_created"):
            self.report({'INFO'}, "トーラスを独立させました")
        return {'FINISHED'}

class OT_detach_cone(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_cone"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Cone", "Detached_Cone", "is_cn_created"):
            self.report({'INFO'}, "円錐を独立させました")
        return {'FINISHED'}

class OT_detach_sphere(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_sphere"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Sphere", "Detached_Sphere", "is_sp_created"):
            self.report({'INFO'}, "球体を独立させました")
        return {'FINISHED'}

class OT_detach_arrows(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_arrows"
    bl_label = "切り離し(独立)"
    def execute(self, context):
        if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Arrows", "Detached_Arrows", "is_aw_created"):
            self.report({'INFO'}, "矢印を独立させました")
        return {'FINISHED'}

class OT_open_url(bpy.types.Operator):
    bl_idname = f"{PREFIX}.open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

# --- 安全な遅延アンインストール関数 ---
def delayed_unregister():
    try:
        # アドオンとしてインストールされている場合は完全に無効化する
        addon_utils.disable(__name__)
    except Exception:
        pass

    try:
        unregister()
    except Exception as e:
        print(f"[{ADDON_TITLE}] Delayed Unregister Error: {e}")
    return None

class OT_remove_addon(bpy.types.Operator):
    bl_idname = f"{PREFIX}.remove_addon"
    bl_label = "Clean up & Uninstall"
    bl_description = "生成したデータを全て消去し、アドオンを無効化してUIを閉じます"
    
    def execute(self, context):
        # 1. データのクリーンアップ(自己アンインストール)
        col_name = f"{PREFIX_VAL}_Collection"
        col = bpy.data.collections.get(col_name)
        if col:
            # コレクション内のオブジェクトを全削除
            for obj in list(col.objects):
                bpy.data.objects.remove(obj, do_unlink=True)
            bpy.data.collections.remove(col)
            
        # 不要なメッシュデータを削除
        for mesh in list(bpy.data.meshes):
            if mesh.name.startswith(PREFIX_VAL):
                bpy.data.meshes.remove(mesh, do_unlink=True)
                
        # 不要なマテリアルデータを削除
        for mat in list(bpy.data.materials):
            if mat.name.startswith(f"{PREFIX_VAL}_Mat"):
                bpy.data.materials.remove(mat, do_unlink=True)

        # 2. 安全な遅延アンインストール(UI描画ループ外で0.2秒後に実行)
        bpy.app.timers.register(delayed_unregister, first_interval=0.2)
        
        self.report({'INFO'}, "アドオンのデータをクリーンアップし、終了しました")
        return {'FINISHED'}

# =========================================================
# 10. パネル構成 (UI)
# =========================================================
class PT_Base(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

class PT_Torus(PT_Base):
    bl_label = "1. トーラス (Torus)"
    bl_idname = f"{PREFIX_VAL}_PT_torus"
    bl_order = 1
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "tr_loc")
        col.prop(props, "tr_rot")
        col.separator()
        col.prop(props, "tr_mj_rd")
        col.prop(props, "tr_mn_rd")
        col.prop(props, "tr_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_tr_created:
            row.operator(OT_create_torus.bl_idname, icon='MESH_TORUS')
        else:
            row.operator(OT_create_torus.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_torus.bl_idname, icon='UNLINKED')

class PT_Cone(PT_Base):
    bl_label = "2. 円錐 (Cone)"
    bl_idname = f"{PREFIX_VAL}_PT_cone"
    bl_order = 2
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "cn_loc")
        col.prop(props, "cn_rot")
        col.separator()
        col.prop(props, "cn_rd")
        col.prop(props, "cn_ht")
        col.prop(props, "cn_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_cn_created:
            row.operator(OT_create_cone.bl_idname, icon='MESH_CONE')
        else:
            row.operator(OT_create_cone.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_cone.bl_idname, icon='UNLINKED')

class PT_Sphere(PT_Base):
    bl_label = "3. 球体 (Sphere)"
    bl_idname = f"{PREFIX_VAL}_PT_sphere"
    bl_order = 3
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        col = layout.column(align=True)
        col.prop(props, "sp_loc")
        col.prop(props, "sp_rot")
        col.separator()
        col.prop(props, "sp_rd")
        col.prop(props, "sp_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_sp_created:
            row.operator(OT_create_sphere.bl_idname, icon='MESH_UVSPHERE')
        else:
            row.operator(OT_create_sphere.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_sphere.bl_idname, icon='UNLINKED')

class PT_Arrows(PT_Base):
    bl_label = "4. 集中矢印 (Arrows)"
    bl_idname = f"{PREFIX_VAL}_PT_arrows"
    bl_order = 4
    def draw(self, context):
        layout = self.layout
        props = context.scene.tcs_arr_props
        
        layout.label(text="※トーラス円周からターゲットへ向かいます", icon='INFO')
        col = layout.column(align=True)
        col.prop(props, "aw_cnt")
        col.prop(props, "aw_tg_loc")
        col.separator()
        col.prop(props, "aw_thk")
        col.prop(props, "aw_cl")
        layout.separator()
        
        row = layout.row(align=True)
        if not props.is_aw_created:
            row.operator(OT_create_arrows.bl_idname, icon='TRACKING')
        else:
            row.operator(OT_create_arrows.bl_idname, text="更新", icon='FILE_REFRESH')
            row.operator(OT_detach_arrows.bl_idname, icon='UNLINKED')

class PT_Links(PT_Base):
    bl_label = "Links"
    bl_idname = f"{PREFIX_VAL}_PT_links"
    bl_order = 10
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        if LINK_NAME_1: box.operator(OT_open_url.bl_idname, text=LINK_NAME_1, icon='URL').url = LINK_URL_1
        if LINK_NAME_2: box.operator(OT_open_url.bl_idname, text=LINK_NAME_2, icon='URL').url = LINK_URL_2
        if LINK_NAME_3: box.operator(OT_open_url.bl_idname, text=LINK_NAME_3, icon='URL').url = LINK_URL_3
        if LINK_NAME_4: box.operator(OT_open_url.bl_idname, text=LINK_NAME_4, icon='URL').url = LINK_URL_4
        if LINK_NAME_5: box.operator(OT_open_url.bl_idname, text=LINK_NAME_5, icon='URL').url = LINK_URL_5

class PT_Remove(PT_Base):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_remove"
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 11
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="生成物を全消去してアドオンを終了します", icon='ERROR')
        box.operator(OT_remove_addon.bl_idname, icon='CANCEL')

# =========================================================
# 11. 自動セットアップ
# =========================================================
def setup_workspace_on_activation():
    """Nパネルの展開とマテリアルプレビューへの切り替えを自動で行う"""
    if not hasattr(bpy.context, 'window_manager') or not bpy.context.window_manager.windows:
        return 0.1
        
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            space.shading.type = 'MATERIAL'
                            space.show_region_ui = True
    except Exception:
        pass
        
    return None

# =========================================================
# 12. 登録と解除
# =========================================================
classes = [
    TCS_ARR_PropertyGroup,
    OT_create_torus, OT_create_cone, OT_create_sphere, OT_create_arrows,
    OT_detach_torus, OT_detach_cone, OT_detach_sphere, OT_detach_arrows,
    OT_open_url, OT_remove_addon,
    PT_Torus, PT_Cone, PT_Sphere, PT_Arrows, PT_Links, PT_Remove
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.tcs_arr_props = bpy.props.PointerProperty(type=TCS_ARR_PropertyGroup)
    
    if not bpy.app.timers.is_registered(setup_workspace_on_activation):
        bpy.app.timers.register(setup_workspace_on_activation, first_interval=0.1)

# --- 修正: より安全な unregister の実装 ---
def unregister():
    try:
        del bpy.types.Scene.tcs_arr_props
    except Exception:
        pass
        
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass
            
    if bpy.app.timers.is_registered(setup_workspace_on_activation):
        bpy.app.timers.unregister(setup_workspace_on_activation)

if __name__ == "__main__":
    register()