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"> 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"> 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">











# =========================================================
# 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()