20261003 円錐頂点矢印 と 球体

bl_info = {
    "name": "Tangent Cone Generator",
    "author": "AI",
    "version": (1, 10, 0),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar > Tangent Cone",
    "description": "コレクション管理改善・円錐と矢印の別色設定・球体ON/OFF対応 (フリーズ対策版)",
    "category": "Add Mesh",
}

import bpy
import bmesh
import math
import webbrowser
import random
from mathutils import Vector, Matrix
from bpy.app.handlers import persistent

# ==========================================
# 初期値
# ==========================================
ADDON_LINKS = {
    "20261003 円錐頂点矢印 と 球体_002": "<https://app.notion.com/p/20261003-3ed691cbf90680a187f7d9c5ea268c2b>",
    "338 Sphere Cam": "<https://app.notion.com/p/338-Sphere-Cam-3c0347b3454a80abbb61ff2cd585792c>",
    "posfie": "<https://posfie.com/@timekagura/t/zionad2022?sort=0>",
    "zionadchat": "<https://x.com/zionadchat>",
    "背後霊カメラ 最新版リンク": "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>",
    "blender コンテナ 20260713": "<https://note.com/zionadmillion/n/n9a47d88fe7b4>",
    "骨格立方体": "<https://app.notion.com/p/351-2017-3cb347b3454a8086b15fec2f69408bd5>",
}

INIT_APEX_LOC   = (0.0, 0.0, 10.0)
INIT_RADIUS     = 1.0
INIT_SEGMENTS   = 128
INIT_DRAW_MODE  = 'CONE'
INIT_ARROW_CNT  = 4

INIT_SHOW_SPHERE       = True

INIT_SPHERE_COLOR      = (0.2, 0.6, 1.0)
INIT_SPHERE_ALPHA      = 0.35
INIT_CONE_COLOR        = (1.0, 0.5, 0.1)
INIT_CONE_ALPHA        = 0.25
INIT_ARROWS_COLOR      = (0.15, 0.9, 0.35)
INIT_ARROWS_ALPHA      = 0.45

INIT_SHOW_SHORT_ARROW  = True
INIT_SHORT_ARROW_COLOR = (1.0, 0.0, 0.0)
INIT_SHORT_ARROW_ALPHA = 1.0

_IS_UPDATING = False
MAIN_COLL_NAME = "Tangent Cone Setup"
FIXED_COLL_NAME = "Tangent Cone Fixed"

# ==========================================
# セットアップ
# ==========================================
@persistent
def setup_workspace_handler(dummy=None):
    wm = bpy.context.window_manager
    if not wm:
        return
    for window in wm.windows:
        screen = window.screen
        if not screen:
            continue
        for area in screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        try:
                            space.show_region_ui = True
                            if space.shading.type != 'MATERIAL':
                                space.shading.type = 'MATERIAL'
                        except:
                            pass

def delayed_setup():
    setup_workspace_handler()
    return None

# ==========================================
# ユーティリティ (Object管理 完全修正)
# ==========================================
def get_or_create_collection(name):
    coll = bpy.data.collections.get(name)
    if coll is None:
        coll = bpy.data.collections.new(name)
        try:
            bpy.context.scene.collection.children.link(coll)
        except RuntimeError:
            pass
    else:
        if bpy.context.scene.collection.children.get(name) is None:
            try:
                bpy.context.scene.collection.children.link(coll)
            except RuntimeError:
                pass
    return coll

def get_or_create_object(name, coll):
    # 1. 対象コレクション優先
    obj = coll.objects.get(name)
    if obj is not None:
        if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
            obj.data = bpy.data.meshes.new(f"{name}_Mesh")
        return obj
    # 2. 全体から再利用
    obj = bpy.data.objects.get(name)
    if obj is not None:
        if coll.objects.get(obj.name) is None:
            try:
                coll.objects.link(obj)
            except RuntimeError:
                pass
        if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
            obj.data = bpy.data.meshes.new(f"{name}_Mesh")
        return obj
    # 3. 新規
    mesh = bpy.data.meshes.new(f"{name}_Mesh")
    obj = bpy.data.objects.new(name, mesh)
    coll.objects.link(obj)
    return obj

def remove_object_by_name(name):
    """指定名のオブジェクトを全コレクションから外して完全削除"""
    obj = bpy.data.objects.get(name)
    if not obj:
        return
    for c in list(obj.users_collection):
        try:
            c.objects.unlink(obj)
        except:
            pass
    try:
        bpy.data.objects.remove(obj, do_unlink=True)
    except:
        pass

def safe_replace_mesh(bm, obj):
    old_mesh = obj.data
    new_mesh_name = old_mesh.name if old_mesh else f"{obj.name}_Mesh"
    new_mesh = bpy.data.meshes.new(new_mesh_name)
    bm.to_mesh(new_mesh)
    bm.free()
    obj.data = new_mesh
    if old_mesh and old_mesh.users == 0:
        try:
            bpy.data.meshes.remove(old_mesh)
        except:
            pass

def assign_material(obj, name, color, alpha, blend='BLEND'):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
    mat.blend_method = blend
    if mat.use_nodes and mat.node_tree:
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf:
            try:
                bsdf.inputs["Base Color"].default_value = (*color[:3], 1.0)
                if "Alpha" in bsdf.inputs:
                    bsdf.inputs["Alpha"].default_value = alpha
                if "Transmission Weight" in bsdf.inputs:
                    bsdf.inputs["Transmission Weight"].default_value = 0.1
            except:
                pass
    if not obj.data.materials:
        obj.data.materials.append(mat)
    else:
        obj.data.materials[0] = mat

def get_plane_eq(N, Cc):
    nx, ny, nz = N.x, N.y, N.z
    d = N.dot(Cc)
    terms = []
    if abs(nx) > 1e-4: terms.append(f"{nx:.2f}x")
    if abs(ny) > 1e-4: terms.append(f"{ny:.2f}y")
    if abs(nz) > 1e-4: terms.append(f"{nz:.2f}z")
    if not terms: terms.append("0")
    return " + ".join(terms).replace("+ -", "- ") + f" = {d:.2f}"

def get_sphere_eq(Cc, rc):
    def fmt_term(axis, val):
        if abs(val) < 1e-4: return f"{axis}^2"
        if val < 0: return f"({axis} + {abs(val):.2f})^2"
        return f"({axis} - {val:.2f})^2"
    return f"{fmt_term('x', Cc.x)} + {fmt_term('y', Cc.y)} + {fmt_term('z', Cc.z)} = {(rc*rc):.2f}"

# ==========================================
# コアロジック
# ==========================================
def update_tangent_cone_logic(self, context=None):
    global _IS_UPDATING
    if not getattr(self, "is_calculated", False):
        return
    if _IS_UPDATING:
        return
    _IS_UPDATING = True
    try:
        if context is None:
            context = bpy.context

        P = Vector(self.apex_loc)
        C = Vector((0.0, 0.0, 0.0))
        R = self.radius
        segments = self.segments
        V = P - C
        d = V.length

        if d <= R:
            self.res_error = "エラー: 頂点が球の内部または表面にあります"
            return
        self.res_error = ""

        zc = (R * R) / d
        rc = R * math.sqrt(max(0.0, d * d - R * R)) / d
        N = V.normalized()
        Cc = C + N * zc
        cone_h = d - zc
        len_tangent = math.sqrt(max(0.0, d * d - R * R))
        len_shortest = d - R

        tmp = Vector((1, 0, 0)) if abs(N.x) < 0.9 else Vector((0, 1, 0))
        U = N.cross(tmp).normalized()
        W = N.cross(U).normalized()

        circle_verts = []
        for i in range(segments):
            theta = 2 * math.pi * i / segments
            circle_verts.append(Cc + rc * (U * math.cos(theta) + W * math.sin(theta)))

        coll = get_or_create_collection(MAIN_COLL_NAME)

        def add_arrow(bm_mesh, P_start, P_end, scale_R):
            vec = P_end - P_start
            L = vec.length
            if L < 1e-5:
                return
            dir = vec.normalized()
            rot_mat = Vector((0,0,1)).rotation_difference(dir).to_matrix().to_4x4()
            r_shaft = scale_R * 0.02
            r_head = scale_R * 0.06
            L_shaft = L * 0.85
            L_head = L * 0.15
            ret_shaft = bmesh.ops.create_cone(bm_mesh, cap_ends=True, cap_tris=False, segments=12, radius1=r_shaft, radius2=r_shaft, depth=L_shaft)
            bmesh.ops.transform(bm_mesh, matrix=rot_mat, verts=ret_shaft['verts'])
            bmesh.ops.transform(bm_mesh, matrix=Matrix.Translation(P_start + dir*(L_shaft*0.5)), verts=ret_shaft['verts'])
            ret_head = bmesh.ops.create_cone(bm_mesh, cap_ends=True, cap_tris=False, segments=12, radius1=r_head, radius2=0.0, depth=L_head)
            bmesh.ops.transform(bm_mesh, matrix=rot_mat, verts=ret_head['verts'])
            bmesh.ops.transform(bm_mesh, matrix=Matrix.Translation(P_start + dir*(L_shaft + L_head*0.5)), verts=ret_head['verts'])

        # --- 1. 球体関連 (チェックボックスでON/OFF) ---
        if self.show_sphere:
            obj_sphere = get_or_create_object("Sphere_Base", coll)
            bm_sphere = bmesh.new()
            bmesh.ops.create_uvsphere(bm_sphere, u_segments=32, v_segments=16, radius=R)
            for f in bm_sphere.faces: f.smooth = True
            safe_replace_mesh(bm_sphere, obj_sphere)
            assign_material(obj_sphere, "Mat_Sphere", self.sphere_color, self.sphere_alpha)

            obj_cap = get_or_create_object("SphericalCap", coll)
            bm_cap = bmesh.new()
            bmesh.ops.create_uvsphere(bm_cap, u_segments=32, v_segments=16, radius=R)
            bm_cap.verts.ensure_lookup_table()
            to_remove = [v for v in bm_cap.verts if (v.co - Cc).dot(N) < -1e-6]
            if to_remove:
                bmesh.ops.delete(bm_cap, geom=to_remove, context='VERTS')
            for f in bm_cap.faces: f.smooth = True
            safe_replace_mesh(bm_cap, obj_cap)
            assign_material(obj_cap, "Mat_Cap", self.sphere_color, self.sphere_alpha)
        else:
            # OFFなら球体オブジェクトをコレクションから削除
            remove_object_by_name("Sphere_Base")
            remove_object_by_name("SphericalCap")

        # --- 2. 円錐スカート vs 矢印複数 (別オブジェクト・別色) ---
        if self.draw_mode == 'CONE':
            # 矢印側を削除
            remove_object_by_name("Arrows_Tangent")
            remove_object_by_name("Cone_Tangent") # 旧名互換削除

            obj_cone = get_or_create_object("Cone_Skirt", coll)
            bm_cone = bmesh.new()
            apex_v = bm_cone.verts.new(P)
            cv = [bm_cone.verts.new(p) for p in circle_verts]
            bm_cone.verts.ensure_lookup_table()
            for i in range(segments):
                bm_cone.faces.new((apex_v, cv[i], cv[(i+1)%segments]))
            for f in bm_cone.faces: f.smooth = True
            safe_replace_mesh(bm_cone, obj_cone)
            assign_material(obj_cone, "Mat_Cone", self.cone_color, self.cone_alpha)

        else: # ARROWS
            remove_object_by_name("Cone_Skirt")
            remove_object_by_name("Cone_Tangent")

            obj_arrows = get_or_create_object("Arrows_Tangent", coll)
            bm_arrows = bmesh.new()
            arrows = self.arrow_count
            for i in range(arrows):
                theta = 2 * math.pi * i / arrows
                v_target = Cc + rc * (U * math.cos(theta) + W * math.sin(theta))
                add_arrow(bm_arrows, P, v_target, R)
            for f in bm_arrows.faces: f.smooth = True
            safe_replace_mesh(bm_arrows, obj_arrows)
            assign_material(obj_arrows, "Mat_Arrows", self.arrows_color, self.arrows_alpha)

        # --- 3. Contact Circle (常に表示) ---
        obj_circle = get_or_create_object("ContactCircle", coll)
        bm_circle = bmesh.new()
        cv_circ = [bm_circle.verts.new(p) for p in circle_verts]
        bm_circle.verts.ensure_lookup_table()
        for i in range(segments):
            bm_circle.edges.new((cv_circ[i], cv_circ[(i+1)%segments]))
        safe_replace_mesh(bm_circle, obj_circle)
        obj_circle.show_wire = True
        assign_material(obj_circle, "Mat_Circle", (1.0, 0.05, 0.05), 1.0, blend='OPAQUE')

        # --- 4. 最短距離矢印 ---
        obj_short = get_or_create_object("ShortestArrow", coll)
        bm_short = bmesh.new()
        if self.show_short_arrow:
            v_target_short = C + N * R
            add_arrow(bm_short, P, v_target_short, R)
            for f in bm_short.faces: f.smooth = True
        safe_replace_mesh(bm_short, obj_short)
        assign_material(obj_short, "Mat_ShortArrow", self.short_arrow_color, self.short_arrow_alpha)

        # 計算結果
        self.res_apex_z = f"{P.z:.2f}"
        self.res_cone_h = f"{cone_h:.2f}"
        self.res_len_tangent = f"{len_tangent:.2f}"
        self.res_len_shortest = f"{len_shortest:.2f}"
        self.res_circle_z = f"{Cc.z:.2f}"
        self.res_circle_r = f"{rc:.2f}"
        self.res_circle_circum = f"{(2 * math.pi * rc):.2f}"
        self.res_plane_eq = get_plane_eq(N, Cc)
        self.res_sphere_eq = get_sphere_eq(Cc, rc)

    except Exception as e:
        print(f"[Tangent Cone] update error: {e}")
        import traceback
        traceback.print_exc()
        self.res_error = f"更新エラー: {e}"
    finally:
        _IS_UPDATING = False

# ==========================================
# オペレーター
# ==========================================
class TANGENTCONE_OT_init(bpy.types.Operator):
    bl_idname = "mesh.tangent_cone_init"
    bl_label = "作成 (新規生成)"
    def execute(self, context):
        props = context.scene.tangent_cone_props
        props.is_calculated = True
        update_tangent_cone_logic(props, context)
        if props.res_error:
            props.is_calculated = False
            self.report({'WARNING'}, props.res_error)
            return {'CANCELLED'}
        return {'FINISHED'}

class TANGENTCONE_OT_detach(bpy.types.Operator):
    bl_idname = "mesh.tangent_cone_detach"
    bl_label = "切り離し (独立化)"
    bl_description = "現在の生成物を独立させ、色も固定して保持します"
    def execute(self, context):
        global _IS_UPDATING
        if _IS_UPDATING:
            self.report({'WARNING'}, "更新中のため少し待ってください")
            return {'CANCELLED'}
        props = context.scene.tangent_cone_props
        suffix = f"_Fixed_{random.randint(1000, 9999)}"
        names = ["Sphere_Base", "Cone_Skirt", "Arrows_Tangent", "Cone_Tangent", "ContactCircle", "SphericalCap", "ShortestArrow"]
        coll = bpy.data.collections.get(MAIN_COLL_NAME)
        if not coll:
            self.report({'WARNING'}, "対象が見つかりません")
            props.is_calculated = False
            return {'CANCELLED'}
        fixed_coll = bpy.data.collections.get(FIXED_COLL_NAME)
        if not fixed_coll:
            fixed_coll = bpy.data.collections.new(FIXED_COLL_NAME)
            try:
                context.scene.collection.children.link(fixed_coll)
            except RuntimeError:
                pass
        for name in names:
            obj = coll.objects.get(name) or bpy.data.objects.get(name)
            if not obj:
                continue
            if obj.users_collection:
                for c in list(obj.users_collection):
                    if c.name == MAIN_COLL_NAME:
                        try:
                            c.objects.unlink(obj)
                        except:
                            pass
            if fixed_coll.objects.get(obj.name) is None:
                try:
                    fixed_coll.objects.link(obj)
                except RuntimeError:
                    pass
            old_name = obj.name
            obj.name = name + suffix
            if obj.data:
                try:
                    obj.data.name = old_name + suffix + "_Mesh"
                except:
                    pass
                for slot in obj.material_slots:
                    if slot.material:
                        if slot.material.users > 1:
                            new_mat = slot.material.copy()
                            new_mat.name = slot.material.name + suffix
                            slot.material = new_mat
                        else:
                            slot.material.name = slot.material.name + suffix
        props.is_calculated = False
        self.report({'INFO'}, f"切り離し完了: {suffix}")
        return {'FINISHED'}

class TANGENTCONE_OT_copy_text(bpy.types.Operator):
    bl_idname = "wm.copy_tangent_cone_text"
    bl_label = "Copy"
    text_to_copy: bpy.props.StringProperty()
    def execute(self, context):
        context.window_manager.clipboard = self.text_to_copy
        self.report({'INFO'}, f"コピー: {self.text_to_copy}")
        return {'FINISHED'}

class TANGENTCONE_OT_copy_all(bpy.types.Operator):
    bl_idname = "wm.copy_tangent_cone_all"
    bl_label = "全結果をコピー"
    def execute(self, context):
        props = context.scene.tangent_cone_props
        lines = [
            f"【計算結果】",
            f"頂点のZ座標: {props.res_apex_z}",
            f"円錐の高さ: {props.res_cone_h}",
            f"接線矢印の長さ(母線): {props.res_len_tangent}",
            f"最短矢印の長さ(球表面へ): {props.res_len_shortest}",
            f"接円のZ座標: {props.res_circle_z}",
            f"接円の半径: {props.res_circle_r}",
            f"接円の円周: {props.res_circle_circum}",
            f"",
            f"【接する円の方程式】",
            f"平面: {props.res_plane_eq}",
            f"球面: {props.res_sphere_eq}",
        ]
        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "コピーしました")
        return {'FINISHED'}

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

class TANGENTCONE_OT_remove_addon(bpy.types.Operator):
    bl_idname = "wm.tangent_cone_remove_addon"
    bl_label = "アドオン削除"
    def execute(self, context):
        unregister()
        return {'FINISHED'}

# ==========================================
# プロパティ
# ==========================================
class TangentConeProperties(bpy.types.PropertyGroup):
    apex_loc: bpy.props.FloatVectorProperty(name="Apex (X,Y,Z)", default=INIT_APEX_LOC, subtype='XYZ', update=update_tangent_cone_logic)
    radius: bpy.props.FloatProperty(name="Sphere Radius", default=INIT_RADIUS, min=0.001, update=update_tangent_cone_logic)
    segments: bpy.props.IntProperty(name="Segments", default=INIT_SEGMENTS, min=16, max=512, update=update_tangent_cone_logic)
    
    show_sphere: bpy.props.BoolProperty(name="球体を作成", description="Sphere_Base と SphericalCap の生成ON/OFF", default=INIT_SHOW_SPHERE, update=update_tangent_cone_logic)

    draw_mode: bpy.props.EnumProperty(
        name="描画モード",
        items=[('CONE', "円錐スカート", ""), ('ARROWS', "矢印複数", "")],
        default=INIT_DRAW_MODE, update=update_tangent_cone_logic
    )
    arrow_count: bpy.props.IntProperty(name="矢印の本数", default=INIT_ARROW_CNT, min=1, max=12, update=update_tangent_cone_logic)
    show_short_arrow: bpy.props.BoolProperty(name="最短距離矢印を表示", default=INIT_SHOW_SHORT_ARROW, update=update_tangent_cone_logic)

    sphere_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_SPHERE_COLOR, min=0.0, max=1.0, update=update_tangent_cone_logic)
    sphere_alpha: bpy.props.FloatProperty(name="透明度", default=INIT_SPHERE_ALPHA, min=0.0, max=1.0, update=update_tangent_cone_logic)
    
    cone_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_CONE_COLOR, min=0.0, max=1.0, update=update_tangent_cone_logic)
    cone_alpha: bpy.props.FloatProperty(name="透明度", default=INIT_CONE_ALPHA, min=0.0, max=1.0, update=update_tangent_cone_logic)

    arrows_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_ARROWS_COLOR, min=0.0, max=1.0, update=update_tangent_cone_logic)
    arrows_alpha: bpy.props.FloatProperty(name="透明度", default=INIT_ARROWS_ALPHA, min=0.0, max=1.0, update=update_tangent_cone_logic)
    
    short_arrow_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_SHORT_ARROW_COLOR, min=0.0, max=1.0, update=update_tangent_cone_logic)
    short_arrow_alpha: bpy.props.FloatProperty(name="透明度", default=INIT_SHORT_ARROW_ALPHA, min=0.0, max=1.0, update=update_tangent_cone_logic)
    
    is_calculated: bpy.props.BoolProperty(default=False)
    res_error: bpy.props.StringProperty(default="")
    res_apex_z: bpy.props.StringProperty()
    res_cone_h: bpy.props.StringProperty()
    res_len_tangent: bpy.props.StringProperty()
    res_len_shortest: bpy.props.StringProperty()
    res_circle_z: bpy.props.StringProperty()
    res_circle_r: bpy.props.StringProperty()
    res_circle_circum: bpy.props.StringProperty()
    res_plane_eq: bpy.props.StringProperty()
    res_sphere_eq: bpy.props.StringProperty()

# ==========================================
# UI
# ==========================================
class VIEW3D_PT_tangent_cone(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Tangent Cone'
    bl_label = "Tangent Cone Generator"

    def draw(self, context):
        layout = self.layout
        props = context.scene.tangent_cone_props

        # 形状
        box_shape = layout.box()
        box_shape.label(text="形状設定", icon='MESH_DATA')
        col = box_shape.column(align=True)
        col.prop(props, "apex_loc")
        col.prop(props, "radius")
        col.prop(props, "segments")
        col.separator()
        col.prop(props, "show_sphere", toggle=True, icon='SPHERE')

        layout.separator()

        # 描画モード
        box_mode = layout.box()
        box_mode.label(text="生成形状の選択", icon='MODIFIER')
        box_mode.prop(props, "draw_mode", expand=True)
        if props.draw_mode == 'ARROWS':
            box_mode.prop(props, "arrow_count")
        box_mode.separator()
        box_mode.prop(props, "show_short_arrow", toggle=True, icon='TRACKING_FORWARDS')

        layout.separator()

        # マテリアル (別色対応)
        box_mat = layout.box()
        box_mat.label(text="マテリアル (別色設定)", icon='MATERIAL')

        # 球体
        row = box_mat.row(align=True)
        row.enabled = props.show_sphere
        row.label(text="球体:")
        row.prop(props, "sphere_color", text="")
        row.prop(props, "sphere_alpha", slider=True)

        # 円錐スカート専用
        row_cone = box_mat.row(align=True)
        row_cone.label(text="円錐スカート:")
        row_cone.prop(props, "cone_color", text="")
        row_cone.prop(props, "cone_alpha", slider=True)
        if props.draw_mode != 'CONE':
            row_cone.enabled = False

        # 矢印複数専用 (新設)
        row_arrows = box_mat.row(align=True)
        row_arrows.label(text="矢印複数:")
        row_arrows.prop(props, "arrows_color", text="")
        row_arrows.prop(props, "arrows_alpha", slider=True)
        if props.draw_mode != 'ARROWS':
            row_arrows.enabled = False

        # 最短矢印
        if props.show_short_arrow:
            row_short = box_mat.row(align=True)
            row_short.label(text="最短矢印:")
            row_short.prop(props, "short_arrow_color", text="")
            row_short.prop(props, "short_arrow_alpha", slider=True)

        layout.separator()
        if not props.is_calculated:
            layout.operator("mesh.tangent_cone_init", text="作成 (新規生成)", icon='MESH_CONE')
        else:
            layout.operator("mesh.tangent_cone_detach", text="切り離し (独立化)", icon='UNLINKED')
        
        if props.res_error:
            layout.separator()
            layout.label(text=props.res_error, icon='ERROR')
            return

        if props.is_calculated:
            layout.separator()
            box = layout.box()
            box.label(text="【計算結果】", icon='INFO')
            def draw_result(parent, label_text, val_text):
                row = parent.row(align=True)
                split = row.split(factor=0.55)
                split.label(text=label_text)
                sub = split.row(align=True)
                sub.label(text=val_text)
                op = sub.operator("wm.copy_tangent_cone_text", text="", icon='COPYDOWN')
                op.text_to_copy = val_text
            col = box.column(align=True)
            draw_result(col, "接線矢印の長さ (母線)", props.res_len_tangent)
            draw_result(col, "最短矢印の長さ", props.res_len_shortest)
            col.separator()
            draw_result(col, "頂点 Z", props.res_apex_z)
            draw_result(col, "高さ", props.res_cone_h)
            draw_result(col, "接円 Z", props.res_circle_z)
            draw_result(col, "接円 半径", props.res_circle_r)
            draw_result(col, "接円 円周", props.res_circle_circum)
            col.separator()
            col.label(text="【接する円の方程式】")
            draw_result(col, "平面", props.res_plane_eq)
            draw_result(col, "球面", props.res_sphere_eq)
            col.separator()
            col.operator("wm.copy_tangent_cone_all", text="すべての結果をコピー", icon='DOCUMENTS')

            # コレクション情報
            layout.separator()
            box_coll = layout.box()
            box_coll.label(text=f"コレクション: {MAIN_COLL_NAME}", icon='OUTLINER_COLLECTION')
            coll = bpy.data.collections.get(MAIN_COLL_NAME)
            if coll:
                for obj in coll.objects:
                    box_coll.label(text=f" - {obj.name}", icon='OBJECT_DATA')

class VIEW3D_PT_tangent_cone_links(bpy.types.Panel):
    bl_label = "Links"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Tangent Cone'
    def draw(self, context):
        layout = self.layout
        for name, url in ADDON_LINKS.items():
            if name and url:
                layout.operator("wm.tangent_cone_open_url", text=name, icon='URL').url = url

class VIEW3D_PT_tangent_cone_remove(bpy.types.Panel):
    bl_label = "Addon Remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Tangent Cone'
    def draw(self, context):
        layout = self.layout
        layout.operator("wm.tangent_cone_remove_addon", icon='CANCEL')

# ==========================================
# 登録
# ==========================================
classes = (
    TANGENTCONE_OT_init,
    TANGENTCONE_OT_detach,
    TANGENTCONE_OT_copy_text,
    TANGENTCONE_OT_copy_all,
    TANGENTCONE_OT_open_url,
    TANGENTCONE_OT_remove_addon,
    TangentConeProperties,
    VIEW3D_PT_tangent_cone,
    VIEW3D_PT_tangent_cone_links,
    VIEW3D_PT_tangent_cone_remove,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.tangent_cone_props = bpy.props.PointerProperty(type=TangentConeProperties)
    if setup_workspace_handler not in bpy.app.handlers.load_post:
        bpy.app.handlers.load_post.append(setup_workspace_handler)
    try:
        bpy.app.timers.register(delayed_setup, first_interval=0.5)
    except:
        pass

def unregister():
    global _IS_UPDATING
    _IS_UPDATING = False
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except:
            pass
    if hasattr(bpy.types.Scene, "tangent_cone_props"):
        del bpy.types.Scene.tangent_cone_props
    if setup_workspace_handler in bpy.app.handlers.load_post:
        bpy.app.handlers.load_post.remove(setup_workspace_handler)

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