円平面 表示されず
bl_info = {
    "name": "Globe Addon",
    "author": "AI",
    "version": (1, 0, 5),
    "blender": (5, 0, 0), # Blender 5.x 専用
    "location": "View3D > Sidebar",
    "description": "Parametric Globe Generator for Blender 5.x",
    "category": "3D View",
}

import bpy
import bmesh
import math
from mathutils import Matrix, Vector

# ==========================================
# 1. コード先頭の設定項目 (ここだけを変更して使い回す)
# ==========================================

TITLE = "Globe Addon"
TAB_NAME = "GlobeTab"
PANEL_TITLE = "Globe Panel"
PREFIX_NAME = "globe_addon"
COLLECTION_MAIN = "Globe_Main"
COLLECTION_SUB = "Globe_Sub"
OBJ_NAME = "GlobeObj"
LINK_URL = "<https://app.notion.com/p/20260708-397f5dacaf438022ad34c17f3d743608>"

SPHERE_RADIUS = 10.0
SPHERE_THICKNESS = 1.0
SPHERE_COLOR = (0.2, 0.5, 0.8, 1.0)
SPHERE_INNER_COLOR = (0.8, 0.4, 0.1, 1.0)

LATITUDE_COUNT = 3
LONGITUDE_COUNT = 4
HOLE_RADIUS = 1.0

# --- 今回追加した要素の初期値 ---
HOLE_RIM_COLOR = (0.2, 0.8, 0.2, 1.0)          # 円面(穴の断面)の色
HOLE_CAP_COLOR = (0.8, 0.8, 0.2, 1.0)          # 球冠(フタ)の色
HOLE_CENTER_SPHERE_COLOR = (0.9, 0.1, 0.9, 1.0)# 中心の球体の色
HOLE_CENTER_SPHERE_RADIUS = 0.5                # 中心の球体の半径

CONE_RADIUS = 2.0
CONE_DEPTH = 5.0
CONE_COLOR = (0.8, 0.2, 0.2, 1.0)

# ==========================================
# 2. 内部用定数 (同じ文字列を繰り返さないための派生変数)
# ==========================================
NAME_SPHERE = f"{OBJ_NAME}_Sphere"
NAME_CUTTERS = f"{OBJ_NAME}_Cutters"
NAME_CONE = f"{OBJ_NAME}_Cone"
NAME_CAPS = f"{OBJ_NAME}_Caps"
NAME_CENTER_SPHERES = f"{OBJ_NAME}_CenterSpheres"

MAT_SPHERE = f"{PREFIX_NAME}_Mat_Sphere"
MAT_SPHERE_INNER = f"{PREFIX_NAME}_Mat_Sphere_Inner"
MAT_SPHERE_RIM = f"{PREFIX_NAME}_Mat_Sphere_Rim"
MAT_CAPS = f"{PREFIX_NAME}_Mat_Caps"
MAT_CENTER_SPHERES = f"{PREFIX_NAME}_Mat_CenterSpheres"
MAT_CONE = f"{PREFIX_NAME}_Mat_Cone"
PROP_ID = f"{PREFIX_NAME}_props"

# ==========================================
# 3. ユーティリティ関数群 (堅牢な生成と更新)
# ==========================================

def update_material_color(mat_name, color):
    mat = bpy.data.materials.get(mat_name)
    if not mat or not mat.use_nodes:
        return mat
    
    for node in mat.node_tree.nodes:
        if node.type == 'BSDF_PRINCIPLED':
            if "Base Color" in node.inputs:
                node.inputs["Base Color"].default_value = color
            break
    return mat

def create_cutters_mesh(mesh, sphere_radius, lat_count, lon_count, hole_radius):
    bm = bmesh.new()
    length = sphere_radius * 2.5 
    
    for i in range(1, lat_count + 1):
        theta = math.pi * i / (lat_count + 1)
        for j in range(lon_count):
            phi = 2 * math.pi * j / lon_count
            
            x = math.sin(theta) * math.cos(phi)
            y = math.sin(theta) * math.sin(phi)
            z = math.cos(theta)
            dir_vec = Vector((x, y, z))
            
            rot_quat = Vector((0, 0, 1)).rotation_difference(dir_vec)
            pos = dir_vec * sphere_radius
            
            bmesh.ops.create_cone(
                bm, cap_ends=True, segments=16,
                radius1=hole_radius, radius2=hole_radius, depth=length,
                matrix=Matrix.Translation(pos) @ rot_quat.to_matrix().to_4x4()
            )
            
    bm.to_mesh(mesh)
    bm.free()

def create_center_spheres_mesh(mesh, sphere_radius, lat_count, lon_count, center_radius):
    bm = bmesh.new()
    
    for i in range(1, lat_count + 1):
        theta = math.pi * i / (lat_count + 1)
        for j in range(lon_count):
            phi = 2 * math.pi * j / lon_count
            
            x = math.sin(theta) * math.cos(phi)
            y = math.sin(theta) * math.sin(phi)
            z = math.cos(theta)
            dir_vec = Vector((x, y, z))
            pos = dir_vec * sphere_radius
            
            bmesh.ops.create_uvsphere(
                bm, u_segments=16, v_segments=8, radius=center_radius,
                matrix=Matrix.Translation(pos)
            )
            
    bm.to_mesh(mesh)
    bm.free()

def update_meshes_and_props(context):
    props = getattr(context.scene, PROP_ID)
    
    obj_sphere = bpy.data.objects.get(NAME_SPHERE)
    obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
    obj_caps = bpy.data.objects.get(NAME_CAPS)
    obj_center_spheres = bpy.data.objects.get(NAME_CENTER_SPHERES)
    obj_cone = bpy.data.objects.get(NAME_CONE)
    
    if obj_sphere:
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=props.sphere_radius)
        bm.to_mesh(obj_sphere.data)
        bm.free()
        
        mod_solid = obj_sphere.modifiers.get("Solidify")
        if mod_solid:
            mod_solid.thickness = props.sphere_thickness
            mod_solid.use_rim = props.show_hole_rim  # 断面の表示切り替え
            
        obj_sphere.location = props.sphere_location
        obj_sphere.rotation_euler = props.sphere_rotation
        
        update_material_color(MAT_SPHERE, props.sphere_color)
        update_material_color(MAT_SPHERE_INNER, props.sphere_inner_color)
        update_material_color(MAT_SPHERE_RIM, props.hole_rim_color)
            
    if obj_cutters:
        create_cutters_mesh(
            obj_cutters.data, 
            props.sphere_radius, props.hole_latitude, props.hole_longitude, props.hole_radius
        )
        
    if obj_caps:
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=props.sphere_radius)
        bm.to_mesh(obj_caps.data)
        bm.free()
        
        mod_solid = obj_caps.modifiers.get("Solidify")
        if mod_solid:
            mod_solid.thickness = props.sphere_thickness
            
        obj_caps.location = props.sphere_location
        obj_caps.rotation_euler = props.sphere_rotation
        
        obj_caps.hide_viewport = not props.show_hole_cap
        obj_caps.hide_render = not props.show_hole_cap
        
        update_material_color(MAT_CAPS, props.hole_cap_color)
        
    if obj_center_spheres:
        create_center_spheres_mesh(
            obj_center_spheres.data,
            props.sphere_radius, props.hole_latitude, props.hole_longitude, props.hole_center_sphere_radius
        )
        obj_center_spheres.location = props.sphere_location
        obj_center_spheres.rotation_euler = props.sphere_rotation
        
        obj_center_spheres.hide_viewport = not props.show_hole_center_sphere
        obj_center_spheres.hide_render = not props.show_hole_center_sphere
        
        update_material_color(MAT_CENTER_SPHERES, props.hole_center_sphere_color)
        
    if obj_cone:
        bm = bmesh.new()
        bmesh.ops.create_cone(
            bm, cap_ends=True, segments=32, radius1=props.cone_radius, radius2=0, depth=props.cone_depth
        )
        bm.to_mesh(obj_cone.data)
        bm.free()
        
        obj_cone.location = props.cone_location
        obj_cone.rotation_euler = props.cone_rotation
        update_material_color(MAT_CONE, props.cone_color)

def setup_base_objects(context):
    for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
        if c_name not in bpy.data.collections:
            new_c = bpy.data.collections.new(c_name)
            context.scene.collection.children.link(new_c)
            
    main_coll = bpy.data.collections.get(COLLECTION_MAIN)
    sub_coll = bpy.data.collections.get(COLLECTION_SUB)
    
    mat_list = [MAT_SPHERE, MAT_SPHERE_INNER, MAT_SPHERE_RIM, MAT_CAPS, MAT_CENTER_SPHERES, MAT_CONE]
    for mat_name in mat_list:
        if mat_name not in bpy.data.materials:
            mat = bpy.data.materials.new(mat_name)
            mat.use_nodes = True
            
    # 1. メイン球体
    if NAME_SPHERE not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_SPHERE)
        obj = bpy.data.objects.new(NAME_SPHERE, mesh)
        main_coll.objects.link(obj)
        
        mod_solid = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
        mod_bool = obj.modifiers.new(name="Boolean", type='BOOLEAN')
        mod_bool.operation = 'DIFFERENCE'
        mod_bool.solver = 'FLOAT'
        
    obj_sphere = bpy.data.objects.get(NAME_SPHERE)
    if obj_sphere:
        if len(obj_sphere.data.materials) < 3:
            obj_sphere.data.materials.clear()
            obj_sphere.data.materials.append(bpy.data.materials[MAT_SPHERE])
            obj_sphere.data.materials.append(bpy.data.materials[MAT_SPHERE_INNER])
            obj_sphere.data.materials.append(bpy.data.materials[MAT_SPHERE_RIM])
        else:
            obj_sphere.data.materials[0] = bpy.data.materials[MAT_SPHERE]
            obj_sphere.data.materials[1] = bpy.data.materials[MAT_SPHERE_INNER]
            obj_sphere.data.materials[2] = bpy.data.materials[MAT_SPHERE_RIM]
            
        mod_solid = obj_sphere.modifiers.get("Solidify")
        if mod_solid:
            mod_solid.material_offset = 1      # 裏面
            mod_solid.material_offset_rim = 2  # 断面
        
    # 2. カッター(穴あけ用)
    if NAME_CUTTERS not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_CUTTERS)
        obj = bpy.data.objects.new(NAME_CUTTERS, mesh)
        sub_coll.objects.link(obj)
        obj.display_type = 'WIRE'
        obj.hide_render = True
        obj.hide_viewport = True
        
    # 3. 球冠(フタ)
    if NAME_CAPS not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_CAPS)
        obj = bpy.data.objects.new(NAME_CAPS, mesh)
        main_coll.objects.link(obj)
        obj.data.materials.append(bpy.data.materials[MAT_CAPS])
        
        mod_bool = obj.modifiers.new(name="Boolean", type='BOOLEAN')
        mod_bool.operation = 'INTERSECT' # カッターの内側のみ抽出
        mod_bool.solver = 'FLOAT'
        mod_solid = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
        
    # 4. 中心の球体
    if NAME_CENTER_SPHERES not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_CENTER_SPHERES)
        obj = bpy.data.objects.new(NAME_CENTER_SPHERES, mesh)
        main_coll.objects.link(obj)
        obj.data.materials.append(bpy.data.materials[MAT_CENTER_SPHERES])
        
    # 5. 円錐
    if NAME_CONE not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_CONE)
        obj = bpy.data.objects.new(NAME_CONE, mesh)
        main_coll.objects.link(obj)
        obj.data.materials.append(bpy.data.materials[MAT_CONE])
        
    # ブーリアン参照の確実な割り当て
    obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
    obj_caps = bpy.data.objects.get(NAME_CAPS)
    if obj_sphere and obj_cutters:
        mod_bool = obj_sphere.modifiers.get("Boolean")
        if mod_bool: mod_bool.object = obj_cutters
    if obj_caps and obj_cutters:
        mod_bool = obj_caps.modifiers.get("Boolean")
        if mod_bool: mod_bool.object = obj_cutters

# ==========================================
# 4. プロパティ定義とリアルタイム更新制御
# ==========================================
is_updating = False

def trigger_update(self, context):
    global is_updating
    if is_updating: return
    is_updating = True
    try:
        update_meshes_and_props(context)
    finally:
        is_updating = False

class ADDON_PG_props(bpy.types.PropertyGroup):
    sphere_radius: bpy.props.FloatProperty(name="半径", default=SPHERE_RADIUS, min=0.01, update=trigger_update)
    sphere_thickness: bpy.props.FloatProperty(name="厚み", default=SPHERE_THICKNESS, update=trigger_update)
    sphere_color: bpy.props.FloatVectorProperty(name="表面色", subtype='COLOR', default=SPHERE_COLOR, size=4, update=trigger_update)
    sphere_inner_color: bpy.props.FloatVectorProperty(name="裏面色", subtype='COLOR', default=SPHERE_INNER_COLOR, size=4, update=trigger_update)
    sphere_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
    sphere_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)
    
    hole_latitude: bpy.props.IntProperty(name="緯度方向個数", default=LATITUDE_COUNT, min=1, update=trigger_update)
    hole_longitude: bpy.props.IntProperty(name="経度方向個数", default=LONGITUDE_COUNT, min=1, update=trigger_update)
    hole_radius: bpy.props.FloatProperty(name="穴半径", default=HOLE_RADIUS, min=0.01, update=trigger_update)
    
    show_hole_rim: bpy.props.BoolProperty(name="円面を表示", default=True, update=trigger_update)
    hole_rim_color: bpy.props.FloatVectorProperty(name="円面色", subtype='COLOR', default=HOLE_RIM_COLOR, size=4, update=trigger_update)
    
    show_hole_cap: bpy.props.BoolProperty(name="球冠(フタ)を表示", default=False, update=trigger_update)
    hole_cap_color: bpy.props.FloatVectorProperty(name="球冠色", subtype='COLOR', default=HOLE_CAP_COLOR, size=4, update=trigger_update)
    
    show_hole_center_sphere: bpy.props.BoolProperty(name="中心球体を表示", default=False, update=trigger_update)
    hole_center_sphere_radius: bpy.props.FloatProperty(name="中心球体 半径", default=HOLE_CENTER_SPHERE_RADIUS, min=0.01, update=trigger_update)
    hole_center_sphere_color: bpy.props.FloatVectorProperty(name="中心球体 色", subtype='COLOR', default=HOLE_CENTER_SPHERE_COLOR, size=4, update=trigger_update)
    
    cone_radius: bpy.props.FloatProperty(name="半径", default=CONE_RADIUS, min=0.01, update=trigger_update)
    cone_depth: bpy.props.FloatProperty(name="高さ", default=CONE_DEPTH, min=0.01, update=trigger_update)
    cone_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=CONE_COLOR, size=4, update=trigger_update)
    cone_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
    cone_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)

# ==========================================
# 5. オペレーター群
# ==========================================
class ADDON_OT_create(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.create"
    bl_label = "作成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        setup_base_objects(context)
        update_meshes_and_props(context)
        return {'FINISHED'}

class ADDON_OT_detach(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.detach"
    bl_label = "切り離し (独立)"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        obj_list = [NAME_SPHERE, NAME_CUTTERS, NAME_CONE, NAME_CAPS, NAME_CENTER_SPHERES]
        for obj_name in obj_list:
            obj = bpy.data.objects.get(obj_name)
            if obj:
                obj.name = f"{obj_name}_独立"
                if obj.data.materials:
                    for i, mat in enumerate(obj.data.materials):
                        if mat:
                            new_mat = mat.copy()
                            new_mat.name = f"{mat.name}_独立"
                            obj.data.materials[i] = new_mat

        for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
            coll = bpy.data.collections.get(c_name)
            if coll: coll.name = f"{c_name}_独立"

        self.report({'INFO'}, "オブジェクトをアドオンから切り離しました。")
        return {'FINISHED'}

class ADDON_OT_reset(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.reset"
    bl_label = "初期値へ戻す"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        global is_updating
        props = getattr(context.scene, PROP_ID)
        
        is_updating = True
        try:
            props.sphere_radius = SPHERE_RADIUS
            props.sphere_thickness = SPHERE_THICKNESS
            props.sphere_color = SPHERE_COLOR
            props.sphere_inner_color = SPHERE_INNER_COLOR
            props.sphere_location = (0, 0, 0)
            props.sphere_rotation = (0, 0, 0)
            
            props.hole_latitude = LATITUDE_COUNT
            props.hole_longitude = LONGITUDE_COUNT
            props.hole_radius = HOLE_RADIUS
            
            props.show_hole_rim = True
            props.hole_rim_color = HOLE_RIM_COLOR
            props.show_hole_cap = False
            props.hole_cap_color = HOLE_CAP_COLOR
            props.show_hole_center_sphere = False
            props.hole_center_sphere_radius = HOLE_CENTER_SPHERE_RADIUS
            props.hole_center_sphere_color = HOLE_CENTER_SPHERE_COLOR
            
            props.cone_radius = CONE_RADIUS
            props.cone_depth = CONE_DEPTH
            props.cone_color = CONE_COLOR
            props.cone_location = (0, 0, 0)
            props.cone_rotation = (0, 0, 0)
        finally:
            is_updating = False
            
        update_meshes_and_props(context)
        return {'FINISHED'}

class ADDON_OT_remove_addon(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.remove_addon"
    bl_label = "アドオン削除"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        def delay_unregister():
            unregister()
            return None
        bpy.app.timers.register(delay_unregister, first_interval=0.1)
        self.report({'INFO'}, "アドオンを終了しました")
        return {'FINISHED'}

# ==========================================
# 6. UIパネル群
# ==========================================
class ADDON_PT_main(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME}_pt_main"
    bl_label = PANEL_TITLE
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROP_ID)
        
        layout.operator("view3d.view_selected", text="選択オブジェクトを画面中央に表示", icon='VIEW_CAMERA')
        layout.separator()
        
        row = layout.row(align=True)
        row.scale_y = 1.2
        row.operator(f"{PREFIX_NAME}.create", text="作成", icon='MESH_UVSPHERE')
        row.operator(f"{PREFIX_NAME}.detach", text="切り離し (独立)", icon='UNLINKED')
        layout.operator(f"{PREFIX_NAME}.reset", text="初期値へ戻す", icon='FILE_REFRESH')
        layout.separator()
        
        box = layout.box()
        box.label(text="③ 球体設定")
        box.prop(props, "sphere_radius")
        box.prop(props, "sphere_thickness")
        box.prop(props, "sphere_color")
        box.prop(props, "sphere_inner_color")
        box.prop(props, "sphere_location")
        box.prop(props, "sphere_rotation")
        
        box = layout.box()
        box.label(text="④ 穴設定")
        box.prop(props, "hole_latitude")
        box.prop(props, "hole_longitude")
        box.prop(props, "hole_radius")
        
        # 断面(Rim)の設定
        box.separator()
        row_rim = box.row()
        row_rim.prop(props, "show_hole_rim", text="円面(断面)を表示", icon='HIDE_OFF' if props.show_hole_rim else 'HIDE_ON')
        if props.show_hole_rim:
            box.prop(props, "hole_rim_color")
            
        # 球冠(Caps)の設定
        box.separator()
        row_cap = box.row()
        row_cap.prop(props, "show_hole_cap", text="球冠(フタ)を表示", icon='HIDE_OFF' if props.show_hole_cap else 'HIDE_ON')
        if props.show_hole_cap:
            box.prop(props, "hole_cap_color")
            
        # 中心球体の設定
        box.separator()
        row_sph = box.row()
        row_sph.prop(props, "show_hole_center_sphere", text="中心球体を表示", icon='HIDE_OFF' if props.show_hole_center_sphere else 'HIDE_ON')
        if props.show_hole_center_sphere:
            box.prop(props, "hole_center_sphere_radius")
            box.prop(props, "hole_center_sphere_color")
        
        box = layout.box()
        box.label(text="⑤ 円錐設定")
        box.prop(props, "cone_radius")
        box.prop(props, "cone_depth")
        box.prop(props, "cone_color")
        box.prop(props, "cone_location")
        box.prop(props, "cone_rotation")

class ADDON_PT_link(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME}_pt_link"
    bl_label = "⑥ リンク"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        op = self.layout.operator("wm.url_open", text="地球儀 穴窓面 20260708", icon='URL')
        op.url = LINK_URL

class ADDON_PT_remove(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME}_pt_remove"
    bl_label = "⑦ アドオン削除"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        self.layout.operator(f"{PREFIX_NAME}.remove_addon", text="アドオン削除", icon='CANCEL')

# ==========================================
# 7. 登録処理
# ==========================================
classes = [
    ADDON_PG_props,
    ADDON_OT_create,
    ADDON_OT_detach,
    ADDON_OT_reset,
    ADDON_OT_remove_addon,
    ADDON_PT_main,
    ADDON_PT_link,
    ADDON_PT_remove,
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    setattr(bpy.types.Scene, PROP_ID, bpy.props.PointerProperty(type=ADDON_PG_props))

def unregister():
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass
    if hasattr(bpy.types.Scene, PROP_ID):
        delattr(bpy.types.Scene, PROP_ID)

if __name__ == "__main__":
    register()
bl_info = {
    "name": "Globe Addon",
    "author": "AI",
    "version": (1, 0, 4),
    "blender": (5, 0, 0), # Blender 5.x 専用
    "location": "View3D > Sidebar",
    "description": "Parametric Globe Generator for Blender 5.x",
    "category": "3D View",
}

import bpy
import bmesh
import math
from mathutils import Matrix, Vector

# ==========================================
# 1. コード先頭の設定項目 (ここだけを変更して使い回す)
# ==========================================

TITLE = "Globe Addon"
TAB_NAME = "GlobeTab"
PANEL_TITLE = "Globe Panel"
PREFIX_NAME = "globe_addon"
COLLECTION_MAIN = "Globe_Main"
COLLECTION_SUB = "Globe_Sub"
OBJ_NAME = "GlobeObj"
LINK_URL = "<https://app.notion.com/p/20260708-397f5dacaf438022ad34c17f3d743608>"

SPHERE_RADIUS = 10.0
SPHERE_THICKNESS = 1.0
SPHERE_COLOR = (0.2, 0.5, 0.8, 1.0)
SPHERE_INNER_COLOR = (0.8, 0.4, 0.1, 1.0)  # 追加: 裏面(内側)のデフォルト色

LATITUDE_COUNT = 3
LONGITUDE_COUNT = 4
HOLE_RADIUS = 1.0

CONE_RADIUS = 2.0
CONE_DEPTH = 5.0
CONE_COLOR = (0.8, 0.2, 0.2, 1.0)

# ==========================================
# 2. 内部用定数 (同じ文字列を繰り返さないための派生変数)
# ==========================================
NAME_SPHERE = f"{OBJ_NAME}_Sphere"
NAME_CUTTERS = f"{OBJ_NAME}_Cutters"
NAME_CONE = f"{OBJ_NAME}_Cone"
MAT_SPHERE = f"{PREFIX_NAME}_Mat_Sphere"
MAT_SPHERE_INNER = f"{PREFIX_NAME}_Mat_Sphere_Inner"  # 追加: 裏面(内側)用マテリアル
MAT_CONE = f"{PREFIX_NAME}_Mat_Cone"
PROP_ID = f"{PREFIX_NAME}_props"

# ==========================================
# 3. ユーティリティ関数群 (堅牢な生成と更新)
# ==========================================

def update_material_color(mat_name, color):
    mat = bpy.data.materials.get(mat_name)
    if not mat or not mat.use_nodes:
        return mat
    
    for node in mat.node_tree.nodes:
        if node.type == 'BSDF_PRINCIPLED':
            if "Base Color" in node.inputs:
                node.inputs["Base Color"].default_value = color
            break
    return mat

def create_cutters_mesh(mesh, sphere_radius, lat_count, lon_count, hole_radius):
    bm = bmesh.new()
    length = sphere_radius * 2.5 
    
    for i in range(1, lat_count + 1):
        theta = math.pi * i / (lat_count + 1)
        for j in range(lon_count):
            phi = 2 * math.pi * j / lon_count
            
            x = math.sin(theta) * math.cos(phi)
            y = math.sin(theta) * math.sin(phi)
            z = math.cos(theta)
            dir_vec = Vector((x, y, z))
            
            rot_quat = Vector((0, 0, 1)).rotation_difference(dir_vec)
            pos = dir_vec * sphere_radius
            
            bmesh.ops.create_cone(
                bm,
                cap_ends=True,
                segments=16,
                radius1=hole_radius,
                radius2=hole_radius,
                depth=length,
                matrix=Matrix.Translation(pos) @ rot_quat.to_matrix().to_4x4()
            )
            
    bm.to_mesh(mesh)
    bm.free()

def update_meshes_and_props(context):
    props = getattr(context.scene, PROP_ID)
    
    obj_sphere = bpy.data.objects.get(NAME_SPHERE)
    obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
    obj_cone = bpy.data.objects.get(NAME_CONE)
    
    if obj_sphere:
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=props.sphere_radius)
        bm.to_mesh(obj_sphere.data)
        bm.free()
        
        mod_solid = obj_sphere.modifiers.get("Solidify")
        if mod_solid:
            mod_solid.thickness = props.sphere_thickness
            
        obj_sphere.location = props.sphere_location
        obj_sphere.rotation_euler = props.sphere_rotation
        
        # 表面と裏面(内側)のマテリアル色をそれぞれ更新
        update_material_color(MAT_SPHERE, props.sphere_color)
        update_material_color(MAT_SPHERE_INNER, props.sphere_inner_color)
            
    if obj_cutters:
        create_cutters_mesh(
            obj_cutters.data, 
            props.sphere_radius, 
            props.hole_latitude, 
            props.hole_longitude, 
            props.hole_radius
        )
        
    if obj_cone:
        bm = bmesh.new()
        bmesh.ops.create_cone(
            bm, 
            cap_ends=True, 
            segments=32, 
            radius1=props.cone_radius, 
            radius2=0, 
            depth=props.cone_depth
        )
        bm.to_mesh(obj_cone.data)
        bm.free()
        
        obj_cone.location = props.cone_location
        obj_cone.rotation_euler = props.cone_rotation
        update_material_color(MAT_CONE, props.cone_color)

def setup_base_objects(context):
    for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
        if c_name not in bpy.data.collections:
            new_c = bpy.data.collections.new(c_name)
            context.scene.collection.children.link(new_c)
            
    main_coll = bpy.data.collections.get(COLLECTION_MAIN)
    sub_coll = bpy.data.collections.get(COLLECTION_SUB)
    
    # 表面、裏面、円錐の3つのマテリアルを確保
    for mat_name in [MAT_SPHERE, MAT_SPHERE_INNER, MAT_CONE]:
        if mat_name not in bpy.data.materials:
            mat = bpy.data.materials.new(mat_name)
            mat.use_nodes = True
            
    if NAME_SPHERE not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_SPHERE)
        obj = bpy.data.objects.new(NAME_SPHERE, mesh)
        main_coll.objects.link(obj)
        
        mod_solid = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
        mod_bool = obj.modifiers.new(name="Boolean", type='BOOLEAN')
        mod_bool.operation = 'DIFFERENCE'
        mod_bool.solver = 'FLOAT'
        
    # 球体のマテリアルスロットとモディファイアの設定を保証する
    obj_sphere = bpy.data.objects.get(NAME_SPHERE)
    if obj_sphere:
        # マテリアルスロットを2つ設定(0:表面, 1:裏面)
        if len(obj_sphere.data.materials) < 2:
            obj_sphere.data.materials.clear()
            obj_sphere.data.materials.append(bpy.data.materials[MAT_SPHERE])
            obj_sphere.data.materials.append(bpy.data.materials[MAT_SPHERE_INNER])
        else:
            obj_sphere.data.materials[0] = bpy.data.materials[MAT_SPHERE]
            obj_sphere.data.materials[1] = bpy.data.materials[MAT_SPHERE_INNER]
            
        mod_solid = obj_sphere.modifiers.get("Solidify")
        if mod_solid:
            mod_solid.material_offset = 1      # 内側の面をスロット1(裏面色)にする
            mod_solid.material_offset_rim = 0  # 穴の断面はスロット0(表面色)にする
        
    if NAME_CUTTERS not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_CUTTERS)
        obj = bpy.data.objects.new(NAME_CUTTERS, mesh)
        sub_coll.objects.link(obj)
        obj.display_type = 'WIRE'
        obj.hide_render = True
        obj.hide_viewport = True
        
    if NAME_CONE not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_CONE)
        obj = bpy.data.objects.new(NAME_CONE, mesh)
        main_coll.objects.link(obj)
        obj.data.materials.append(bpy.data.materials[MAT_CONE])
        
    obj_sphere = bpy.data.objects.get(NAME_SPHERE)
    obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
    if obj_sphere and obj_cutters:
        mod_bool = obj_sphere.modifiers.get("Boolean")
        if mod_bool:
            mod_bool.object = obj_cutters

# ==========================================
# 4. プロパティ定義とリアルタイム更新制御
# ==========================================
is_updating = False

def trigger_update(self, context):
    global is_updating
    if is_updating:
        return
    is_updating = True
    try:
        update_meshes_and_props(context)
    finally:
        is_updating = False

class ADDON_PG_props(bpy.types.PropertyGroup):
    sphere_radius: bpy.props.FloatProperty(name="半径", default=SPHERE_RADIUS, min=0.01, update=trigger_update)
    sphere_thickness: bpy.props.FloatProperty(name="厚み", default=SPHERE_THICKNESS, update=trigger_update)
    sphere_color: bpy.props.FloatVectorProperty(name="表面色", subtype='COLOR', default=SPHERE_COLOR, size=4, update=trigger_update)
    sphere_inner_color: bpy.props.FloatVectorProperty(name="裏面色", subtype='COLOR', default=SPHERE_INNER_COLOR, size=4, update=trigger_update) # 追加: 裏面色
    sphere_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
    sphere_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)
    
    hole_latitude: bpy.props.IntProperty(name="緯度方向個数", default=LATITUDE_COUNT, min=1, update=trigger_update)
    hole_longitude: bpy.props.IntProperty(name="経度方向個数", default=LONGITUDE_COUNT, min=1, update=trigger_update)
    hole_radius: bpy.props.FloatProperty(name="穴半径", default=HOLE_RADIUS, min=0.01, update=trigger_update)
    
    cone_radius: bpy.props.FloatProperty(name="半径", default=CONE_RADIUS, min=0.01, update=trigger_update)
    cone_depth: bpy.props.FloatProperty(name="高さ", default=CONE_DEPTH, min=0.01, update=trigger_update)
    cone_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=CONE_COLOR, size=4, update=trigger_update)
    cone_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
    cone_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)

# ==========================================
# 5. オペレーター群
# ==========================================
class ADDON_OT_create(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.create"
    bl_label = "作成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        setup_base_objects(context)
        update_meshes_and_props(context)
        return {'FINISHED'}

class ADDON_OT_detach(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.detach"
    bl_label = "切り離し (独立)"
    bl_description = "現在のオブジェクトをアドオンの更新対象から切り離し、静的モデルとして独立させます"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        for obj_name in [NAME_SPHERE, NAME_CUTTERS, NAME_CONE]:
            obj = bpy.data.objects.get(obj_name)
            if obj:
                obj.name = f"{obj_name}_独立"
                if obj.data.materials:
                    for i, mat in enumerate(obj.data.materials):
                        if mat:
                            new_mat = mat.copy()
                            new_mat.name = f"{mat.name}_独立"
                            obj.data.materials[i] = new_mat

        for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
            coll = bpy.data.collections.get(c_name)
            if coll:
                coll.name = f"{c_name}_独立"

        self.report({'INFO'}, "オブジェクトをアドオンから切り離しました。")
        return {'FINISHED'}

class ADDON_OT_reset(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.reset"
    bl_label = "初期値へ戻す"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        global is_updating
        props = getattr(context.scene, PROP_ID)
        
        is_updating = True
        try:
            props.sphere_radius = SPHERE_RADIUS
            props.sphere_thickness = SPHERE_THICKNESS
            props.sphere_color = SPHERE_COLOR
            props.sphere_inner_color = SPHERE_INNER_COLOR # リセットにも対応
            props.sphere_location = (0, 0, 0)
            props.sphere_rotation = (0, 0, 0)
            
            props.hole_latitude = LATITUDE_COUNT
            props.hole_longitude = LONGITUDE_COUNT
            props.hole_radius = HOLE_RADIUS
            
            props.cone_radius = CONE_RADIUS
            props.cone_depth = CONE_DEPTH
            props.cone_color = CONE_COLOR
            props.cone_location = (0, 0, 0)
            props.cone_rotation = (0, 0, 0)
        finally:
            is_updating = False
            
        update_meshes_and_props(context)
        return {'FINISHED'}

class ADDON_OT_remove_addon(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.remove_addon"
    bl_label = "アドオン削除"
    bl_description = "このアドオンのパネルをUIから完全に削除(終了)します"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        def delay_unregister():
            unregister()
            return None
        
        bpy.app.timers.register(delay_unregister, first_interval=0.1)
        self.report({'INFO'}, "アドオンを終了しました")
        return {'FINISHED'}

# ==========================================
# 6. UIパネル群
# ==========================================
class ADDON_PT_main(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME}_pt_main"
    bl_label = PANEL_TITLE
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROP_ID)
        
        layout.operator("view3d.view_selected", text="選択オブジェクトを画面中央に表示", icon='VIEW_CAMERA')
        layout.separator()
        
        row = layout.row(align=True)
        row.scale_y = 1.2
        row.operator(f"{PREFIX_NAME}.create", text="作成", icon='MESH_UVSPHERE')
        row.operator(f"{PREFIX_NAME}.detach", text="切り離し (独立)", icon='UNLINKED')
        layout.operator(f"{PREFIX_NAME}.reset", text="初期値へ戻す", icon='FILE_REFRESH')
        layout.separator()
        
        box = layout.box()
        box.label(text="③ 球体設定")
        box.prop(props, "sphere_radius")
        box.prop(props, "sphere_thickness")
        box.prop(props, "sphere_color")
        box.prop(props, "sphere_inner_color") # UIに裏面色を追加
        box.prop(props, "sphere_location")
        box.prop(props, "sphere_rotation")
        
        box = layout.box()
        box.label(text="④ 穴設定")
        box.prop(props, "hole_latitude")
        box.prop(props, "hole_longitude")
        box.prop(props, "hole_radius")
        
        box = layout.box()
        box.label(text="⑤ 円錐設定")
        box.prop(props, "cone_radius")
        box.prop(props, "cone_depth")
        box.prop(props, "cone_color")
        box.prop(props, "cone_location")
        box.prop(props, "cone_rotation")

class ADDON_PT_link(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME}_pt_link"
    bl_label = "⑥ リンク"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        op = self.layout.operator("wm.url_open", text="地球儀 穴窓面 20260708", icon='URL')
        op.url = LINK_URL

class ADDON_PT_remove(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME}_pt_remove"
    bl_label = "⑦ アドオン削除"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        self.layout.operator(f"{PREFIX_NAME}.remove_addon", text="アドオン削除", icon='CANCEL')

# ==========================================
# 7. 登録処理
# ==========================================
classes = [
    ADDON_PG_props,
    ADDON_OT_create,
    ADDON_OT_detach,
    ADDON_OT_reset,
    ADDON_OT_remove_addon,
    ADDON_PT_main,
    ADDON_PT_link,
    ADDON_PT_remove,
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    setattr(bpy.types.Scene, PROP_ID, bpy.props.PointerProperty(type=ADDON_PG_props))

def unregister():
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass
    if hasattr(bpy.types.Scene, PROP_ID):
        delattr(bpy.types.Scene, PROP_ID)

if __name__ == "__main__":
    register()
bl_info = {
    "name": "Globe Addon",
    "author": "AI",
    "version": (1, 0, 3),
    "blender": (5, 0, 0), # Blender 5.x 専用
    "location": "View3D > Sidebar",
    "description": "Parametric Globe Generator for Blender 5.x",
    "category": "3D View",
}

import bpy
import bmesh
import math
from mathutils import Matrix, Vector

# ==========================================
# 1. コード先頭の設定項目 (ここだけを変更して使い回す)
# ==========================================

TITLE = "Globe Addon"
TAB_NAME = "GlobeTab"
PANEL_TITLE = "Globe Panel"
PREFIX_NAME = "globe_addon"
COLLECTION_MAIN = "Globe_Main"
COLLECTION_SUB = "Globe_Sub"
OBJ_NAME = "GlobeObj"
LINK_URL = "<https://app.notion.com/p/20260708-397f5dacaf438022ad34c17f3d743608>"

SPHERE_RADIUS = 10.0
SPHERE_THICKNESS = 1.0
SPHERE_COLOR = (0.2, 0.5, 0.8, 1.0)

LATITUDE_COUNT = 3
LONGITUDE_COUNT = 4
HOLE_RADIUS = 1.0  # パネル仕様「穴半径」用の初期値

CONE_RADIUS = 2.0
CONE_DEPTH = 5.0
CONE_COLOR = (0.8, 0.2, 0.2, 1.0)

# ==========================================
# 2. 内部用定数 (同じ文字列を繰り返さないための派生変数)
# ==========================================
NAME_SPHERE = f"{OBJ_NAME}_Sphere"
NAME_CUTTERS = f"{OBJ_NAME}_Cutters"
NAME_CONE = f"{OBJ_NAME}_Cone"
MAT_SPHERE = f"{PREFIX_NAME}_Mat_Sphere"
MAT_CONE = f"{PREFIX_NAME}_Mat_Cone"
PROP_ID = f"{PREFIX_NAME}_props"

# ==========================================
# 3. ユーティリティ関数群 (堅牢な生成と更新)
# ==========================================

def update_material_color(mat_name, color):
    mat = bpy.data.materials.get(mat_name)
    if not mat or not mat.use_nodes:
        return mat
    
    for node in mat.node_tree.nodes:
        if node.type == 'BSDF_PRINCIPLED':
            if "Base Color" in node.inputs:
                node.inputs["Base Color"].default_value = color
            break
    return mat

def create_cutters_mesh(mesh, sphere_radius, lat_count, lon_count, hole_radius):
    bm = bmesh.new()
    length = sphere_radius * 2.5 
    
    for i in range(1, lat_count + 1):
        theta = math.pi * i / (lat_count + 1)
        for j in range(lon_count):
            phi = 2 * math.pi * j / lon_count
            
            x = math.sin(theta) * math.cos(phi)
            y = math.sin(theta) * math.sin(phi)
            z = math.cos(theta)
            dir_vec = Vector((x, y, z))
            
            rot_quat = Vector((0, 0, 1)).rotation_difference(dir_vec)
            pos = dir_vec * sphere_radius
            
            bmesh.ops.create_cone(
                bm,
                cap_ends=True,
                segments=16,
                radius1=hole_radius,
                radius2=hole_radius,
                depth=length,
                matrix=Matrix.Translation(pos) @ rot_quat.to_matrix().to_4x4()
            )
            
    bm.to_mesh(mesh)
    bm.free()

def update_meshes_and_props(context):
    props = getattr(context.scene, PROP_ID)
    
    obj_sphere = bpy.data.objects.get(NAME_SPHERE)
    obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
    obj_cone = bpy.data.objects.get(NAME_CONE)
    
    if obj_sphere:
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=props.sphere_radius)
        bm.to_mesh(obj_sphere.data)
        bm.free()
        
        mod_solid = obj_sphere.modifiers.get("Solidify")
        if mod_solid:
            mod_solid.thickness = props.sphere_thickness
            
        obj_sphere.location = props.sphere_location
        obj_sphere.rotation_euler = props.sphere_rotation
        update_material_color(MAT_SPHERE, props.sphere_color)
            
    if obj_cutters:
        create_cutters_mesh(
            obj_cutters.data, 
            props.sphere_radius, 
            props.hole_latitude, 
            props.hole_longitude, 
            props.hole_radius
        )
        
    if obj_cone:
        bm = bmesh.new()
        bmesh.ops.create_cone(
            bm, 
            cap_ends=True, 
            segments=32, 
            radius1=props.cone_radius, 
            radius2=0, 
            depth=props.cone_depth
        )
        bm.to_mesh(obj_cone.data)
        bm.free()
        
        obj_cone.location = props.cone_location
        obj_cone.rotation_euler = props.cone_rotation
        update_material_color(MAT_CONE, props.cone_color)

def setup_base_objects(context):
    for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
        if c_name not in bpy.data.collections:
            new_c = bpy.data.collections.new(c_name)
            context.scene.collection.children.link(new_c)
            
    main_coll = bpy.data.collections.get(COLLECTION_MAIN)
    sub_coll = bpy.data.collections.get(COLLECTION_SUB)
    
    for mat_name in [MAT_SPHERE, MAT_CONE]:
        if mat_name not in bpy.data.materials:
            mat = bpy.data.materials.new(mat_name)
            mat.use_nodes = True
            
    if NAME_SPHERE not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_SPHERE)
        obj = bpy.data.objects.new(NAME_SPHERE, mesh)
        main_coll.objects.link(obj)
        obj.data.materials.append(bpy.data.materials[MAT_SPHERE])
        
        mod_solid = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
        mod_bool = obj.modifiers.new(name="Boolean", type='BOOLEAN')
        mod_bool.operation = 'DIFFERENCE'
        mod_bool.solver = 'FLOAT'  # Blender 5.x 用
        
    if NAME_CUTTERS not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_CUTTERS)
        obj = bpy.data.objects.new(NAME_CUTTERS, mesh)
        sub_coll.objects.link(obj)
        obj.display_type = 'WIRE'
        obj.hide_render = True
        obj.hide_viewport = True
        
    if NAME_CONE not in bpy.data.objects:
        mesh = bpy.data.meshes.new(NAME_CONE)
        obj = bpy.data.objects.new(NAME_CONE, mesh)
        main_coll.objects.link(obj)
        obj.data.materials.append(bpy.data.materials[MAT_CONE])
        
    obj_sphere = bpy.data.objects.get(NAME_SPHERE)
    obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
    if obj_sphere and obj_cutters:
        mod_bool = obj_sphere.modifiers.get("Boolean")
        if mod_bool:
            mod_bool.object = obj_cutters

# ==========================================
# 4. プロパティ定義とリアルタイム更新制御
# ==========================================
is_updating = False

def trigger_update(self, context):
    global is_updating
    if is_updating:
        return
    is_updating = True
    try:
        update_meshes_and_props(context)
    finally:
        is_updating = False

class ADDON_PG_props(bpy.types.PropertyGroup):
    sphere_radius: bpy.props.FloatProperty(name="半径", default=SPHERE_RADIUS, min=0.01, update=trigger_update)
    sphere_thickness: bpy.props.FloatProperty(name="厚み", default=SPHERE_THICKNESS, update=trigger_update)
    sphere_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=SPHERE_COLOR, size=4, update=trigger_update)
    sphere_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
    sphere_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)
    
    hole_latitude: bpy.props.IntProperty(name="緯度方向個数", default=LATITUDE_COUNT, min=1, update=trigger_update)
    hole_longitude: bpy.props.IntProperty(name="経度方向個数", default=LONGITUDE_COUNT, min=1, update=trigger_update)
    hole_radius: bpy.props.FloatProperty(name="穴半径", default=HOLE_RADIUS, min=0.01, update=trigger_update)
    
    cone_radius: bpy.props.FloatProperty(name="半径", default=CONE_RADIUS, min=0.01, update=trigger_update)
    cone_depth: bpy.props.FloatProperty(name="高さ", default=CONE_DEPTH, min=0.01, update=trigger_update)
    cone_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=CONE_COLOR, size=4, update=trigger_update)
    cone_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
    cone_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)

# ==========================================
# 5. オペレーター群
# ==========================================
class ADDON_OT_create(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.create"
    bl_label = "作成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        setup_base_objects(context)
        update_meshes_and_props(context)
        return {'FINISHED'}

class ADDON_OT_detach(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.detach"
    bl_label = "切り離し (独立)"
    bl_description = "現在のオブジェクトをアドオンの更新対象から切り離し、静的モデルとして独立させます"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        # 1. オブジェクトとマテリアルのリネーム
        for obj_name in [NAME_SPHERE, NAME_CUTTERS, NAME_CONE]:
            obj = bpy.data.objects.get(obj_name)
            if obj:
                obj.name = f"{obj_name}_独立"
                if obj.data.materials:
                    for i, mat in enumerate(obj.data.materials):
                        if mat:
                            new_mat = mat.copy()
                            new_mat.name = f"{mat.name}_独立"
                            obj.data.materials[i] = new_mat

        # 2. コレクションのリネーム
        for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
            coll = bpy.data.collections.get(c_name)
            if coll:
                coll.name = f"{c_name}_独立"

        self.report({'INFO'}, "オブジェクトをアドオンから切り離しました。")
        return {'FINISHED'}

class ADDON_OT_reset(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.reset"
    bl_label = "初期値へ戻す"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        global is_updating
        props = getattr(context.scene, PROP_ID)
        
        is_updating = True
        try:
            props.sphere_radius = SPHERE_RADIUS
            props.sphere_thickness = SPHERE_THICKNESS
            props.sphere_color = SPHERE_COLOR
            props.sphere_location = (0, 0, 0)
            props.sphere_rotation = (0, 0, 0)
            
            props.hole_latitude = LATITUDE_COUNT
            props.hole_longitude = LONGITUDE_COUNT
            props.hole_radius = HOLE_RADIUS
            
            props.cone_radius = CONE_RADIUS
            props.cone_depth = CONE_DEPTH
            props.cone_color = CONE_COLOR
            props.cone_location = (0, 0, 0)
            props.cone_rotation = (0, 0, 0)
        finally:
            is_updating = False
            
        update_meshes_and_props(context)
        return {'FINISHED'}

# 修正箇所: タイマーを利用して実行後に安全にアドオンを解除するように変更
class ADDON_OT_remove_addon(bpy.types.Operator):
    """アドオンを終了し、UIパネルを削除します"""
    bl_idname = f"{PREFIX_NAME}.remove_addon"
    bl_label = "アドオン削除"
    bl_description = "このアドオンのパネルをUIから完全に削除(終了)します"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        # UI更新・オペレータ実行中に自身を削除するとエラーになるため
        # 処理完了の少し後(0.1秒後)に登録解除を実行させます。
        def delay_unregister():
            unregister()
            return None
        
        bpy.app.timers.register(delay_unregister, first_interval=0.1)
        self.report({'INFO'}, "アドオンを終了しました")
        return {'FINISHED'}

# ==========================================
# 6. UIパネル群
# ==========================================
class ADDON_PT_main(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME}_pt_main"
    bl_label = PANEL_TITLE
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROP_ID)
        
        # ① 選択オブジェクト表示
        layout.operator("view3d.view_selected", text="選択オブジェクトを画面中央に表示", icon='VIEW_CAMERA')
        layout.separator()
        
        # ② オブジェクト作成 & 切り離し
        row = layout.row(align=True)
        row.scale_y = 1.2
        row.operator(f"{PREFIX_NAME}.create", text="作成", icon='MESH_UVSPHERE')
        row.operator(f"{PREFIX_NAME}.detach", text="切り離し (独立)", icon='UNLINKED')
        layout.operator(f"{PREFIX_NAME}.reset", text="初期値へ戻す", icon='FILE_REFRESH')
        layout.separator()
        
        # ③ 球体設定
        box = layout.box()
        box.label(text="③ 球体設定")
        box.prop(props, "sphere_radius")
        box.prop(props, "sphere_thickness")
        box.prop(props, "sphere_color")
        box.prop(props, "sphere_location")
        box.prop(props, "sphere_rotation")
        
        # ④ 穴設定
        box = layout.box()
        box.label(text="④ 穴設定")
        box.prop(props, "hole_latitude")
        box.prop(props, "hole_longitude")
        box.prop(props, "hole_radius")
        
        # ⑤ 円錐設定
        box = layout.box()
        box.label(text="⑤ 円錐設定")
        box.prop(props, "cone_radius")
        box.prop(props, "cone_depth")
        box.prop(props, "cone_color")
        box.prop(props, "cone_location")
        box.prop(props, "cone_rotation")

class ADDON_PT_link(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME}_pt_link"
    bl_label = "⑥ リンク"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        op = self.layout.operator("wm.url_open", text="地球儀 穴窓面 20260708", icon='URL')
        op.url = LINK_URL

class ADDON_PT_remove(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME}_pt_remove"
    bl_label = "⑦ アドオン削除"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        self.layout.operator(f"{PREFIX_NAME}.remove_addon", text="アドオン削除", icon='CANCEL')

# ==========================================
# 7. 登録処理
# ==========================================
classes = [
    ADDON_PG_props,
    ADDON_OT_create,
    ADDON_OT_detach,
    ADDON_OT_reset,
    ADDON_OT_remove_addon,
    ADDON_PT_main,
    ADDON_PT_link,
    ADDON_PT_remove,
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    setattr(bpy.types.Scene, PROP_ID, bpy.props.PointerProperty(type=ADDON_PG_props))

def unregister():
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass # 登録解除時のエラーを無視して続行する
    if hasattr(bpy.types.Scene, PROP_ID):
        delattr(bpy.types.Scene, PROP_ID)

if __name__ == "__main__":
    register()