20260604 blender million

1号完成

2号成功

3号成功

rapture_20260709020224.png

bl_info = {
    "name": "Globe Addon",
    "author": "AI",
    "version": (1, 2, 8),
    "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>"

# 初期値 (分割数)
SEG_U_MAIN = 64
SEG_V_MAIN = 32
SEG_CIRCLE = 64
SEG_U_SUB = 32
SEG_V_SUB = 16

# 初期値 (サイズ・色など)
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

PLANE_COLOR = (0.9, 0.9, 0.9, 1.0)       
CAP_COLOR = (0.2, 0.8, 0.4, 1.0)         
CENTER_SPHERE_RADIUS = 1.0
CENTER_SPHERE_COLOR = (0.9, 0.1, 0.1, 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_PLANES = f"{OBJ_NAME}_Planes"
NAME_CAPS = f"{OBJ_NAME}_Caps"
NAME_CENTER_SPHERES = f"{OBJ_NAME}_CenterSpheres"
NAME_CONE = f"{OBJ_NAME}_Cone"

MAT_SPHERE = f"{PREFIX_NAME}_Mat_Sphere"
MAT_SPHERE_INNER = f"{PREFIX_NAME}_Mat_Sphere_Inner"
MAT_PLANE = f"{PREFIX_NAME}_Mat_Plane"
MAT_CAP = f"{PREFIX_NAME}_Mat_Cap"
MAT_CENTER_SPHERE = f"{PREFIX_NAME}_Mat_Center_Sphere"
MAT_CONE = f"{PREFIX_NAME}_Mat_Cone"

PROP_ID = f"{PREFIX_NAME}_props"

# ==========================================
# 3. ユーティリティ関数群
# ==========================================

def ensure_layer_visibility(context, coll_name):
    def traverse(layer_coll):
        if layer_coll.collection.name == coll_name:
            return layer_coll
        for child in layer_coll.children:
            res = traverse(child)
            if res:
                return res
        return None
    lc = traverse(context.view_layer.layer_collection)
    if lc:
        lc.exclude = False
        lc.hide_viewport = False

def get_or_create_collection(coll_name, context):
    coll = bpy.data.collections.get(coll_name)
    if not coll:
        coll = bpy.data.collections.new(coll_name)
    if coll.name not in context.scene.collection.children.keys():
        try:
            context.scene.collection.children.link(coll)
        except Exception:
            pass
    ensure_layer_visibility(context, coll_name)
    return coll

def get_or_create_mesh_object(obj_name, coll):
    obj = bpy.data.objects.get(obj_name)
    if not obj:
        mesh = bpy.data.meshes.new(obj_name)
        obj = bpy.data.objects.new(obj_name, mesh)
        try:
            coll.objects.link(obj)
        except Exception:
            pass
    else:
        if obj.name not in coll.objects:
            try:
                coll.objects.link(obj)
            except Exception:
                pass
        if not obj.data or type(obj.data).__name__ != 'Mesh':
            mesh = bpy.data.meshes.new(obj_name)
            obj.data = mesh
    return obj

def ensure_material(mat_name):
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(mat_name)
        mat.use_nodes = True
    return mat

def assign_material(obj, mat, slot_index=0):
    if len(obj.data.materials) <= slot_index:
        while len(obj.data.materials) <= slot_index:
            obj.data.materials.append(None)
    obj.data.materials[slot_index] = mat

def update_material_color(mat_name, color):
    mat = bpy.data.materials.get(mat_name)
    if not mat or not mat.use_nodes:
        return mat
    
    # 透過表示のための設定(Eevee等)
    if hasattr(mat, "blend_method"):
        mat.blend_method = 'BLEND'
    if hasattr(mat, "shadow_method"):
        mat.shadow_method = 'HASHED'

    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
            # 透明度(Alpha)の適用
            if "Alpha" in node.inputs:
                node.inputs["Alpha"].default_value = color[3] 
            break
    return mat

def apply_smooth_shade(bm, use_smooth):
    for face in bm.faces:
        face.smooth = use_smooth

# --- メッシュ生成 ---
def create_cutters_mesh(mesh, sphere_radius, lat_count, lon_count, hole_radius, segments, use_smooth):
    bm = bmesh.new()
    length = sphere_radius * 2.5 
    dist = math.sqrt(max(0.0, sphere_radius**2 - hole_radius**2))
    
    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 * dist
            
            bmesh.ops.create_cone(
                bm, cap_ends=True, segments=segments,
                radius1=hole_radius, radius2=hole_radius, depth=length,
                matrix=Matrix.Translation(pos) @ rot_quat.to_matrix().to_4x4()
            )
            
    apply_smooth_shade(bm, use_smooth)
    bm.to_mesh(mesh)
    bm.free()

def create_planes_mesh(mesh, sphere_radius, lat_count, lon_count, hole_radius, segments, use_smooth):
    bm = bmesh.new()
    dist = math.sqrt(max(0.0, sphere_radius**2 - hole_radius**2))
    
    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 * dist
            mat_transform = Matrix.Translation(pos) @ rot_quat.to_matrix().to_4x4()
            
            geom = bmesh.ops.create_circle(
                bm, segments=segments, radius=hole_radius, matrix=mat_transform
            )
            verts = geom['verts']
            if len(verts) >= 3:
                try:
                    bm.faces.new(verts)
                except Exception:
                    try:
                        bmesh.ops.contextual_create(bm, geom=verts)
                    except:
                        pass
                        
    apply_smooth_shade(bm, use_smooth)
    bm.to_mesh(mesh)
    bm.free()

def create_center_spheres_mesh(mesh, sphere_radius, lat_count, lon_count, center_radius, hole_radius, seg_u, seg_v, use_smooth, mode):
    bm = bmesh.new()
    dist = math.sqrt(max(0.0, sphere_radius**2 - hole_radius**2))
    
    # 1. テンプレートとなる1つの球体(または半球)を作成
    bmesh.ops.create_uvsphere(bm, u_segments=seg_u, v_segments=seg_v, radius=center_radius)
    
    if mode == 'OUTER':
        # Z < 0 の頂点を削除(外側半球を残す)
        bmesh.ops.delete(bm, geom=[v for v in bm.verts if v.co.z < -0.0001], context='VERTS')
        # 切断面のフタを閉じる
        bmesh.ops.holes_fill(bm, edges=[e for e in bm.edges if e.is_boundary])
    elif mode == 'INNER':
        # Z > 0 の頂点を削除(内側半球を残す)
        bmesh.ops.delete(bm, geom=[v for v in bm.verts if v.co.z > 0.0001], context='VERTS')
        bmesh.ops.holes_fill(bm, edges=[e for e in bm.edges if e.is_boundary])
        
    base_geom = bm.verts[:] + bm.edges[:] + bm.faces[:]

    # 2. テンプレートを複製して各位置に配置
    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 * dist
            rot_quat = Vector((0, 0, 1)).rotation_difference(dir_vec)
            
            # 回転と移動の行列
            mat = Matrix.Translation(pos) @ rot_quat.to_matrix().to_4x4()
            
            # 複製して変形
            res = bmesh.ops.duplicate(bm, geom=base_geom)
            new_verts = [elem for elem in res['geom'] if isinstance(elem, bmesh.types.BMVert)]
            bmesh.ops.transform(bm, matrix=mat, verts=new_verts)
            
    # 3. 最初に作成したテンプレート本体(原点にあるもの)を削除
    bmesh.ops.delete(bm, geom=base_geom, context='VERTS')
    
    # 法線の再計算とスムーズシェード適用
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    apply_smooth_shade(bm, use_smooth)
    
    bm.to_mesh(mesh)
    bm.free()

def setup_base_objects(context):
    main_coll = get_or_create_collection(COLLECTION_MAIN, context)
    sub_coll = get_or_create_collection(COLLECTION_SUB, context)
    
    mat_sphere = ensure_material(MAT_SPHERE)
    mat_sphere_inner = ensure_material(MAT_SPHERE_INNER)
    mat_plane = ensure_material(MAT_PLANE)
    mat_cap = ensure_material(MAT_CAP)
    mat_center = ensure_material(MAT_CENTER_SPHERE)
    mat_cone = ensure_material(MAT_CONE)
            
    # 1. メイン球体
    obj_sphere = get_or_create_mesh_object(NAME_SPHERE, main_coll)
    assign_material(obj_sphere, mat_sphere, 0)
    assign_material(obj_sphere, mat_sphere_inner, 1)
    
    mod_solid = obj_sphere.modifiers.get("Solidify") or obj_sphere.modifiers.new(name="Solidify", type='SOLIDIFY')
    mod_solid.material_offset = 1     
    mod_bool = obj_sphere.modifiers.get("Boolean") or obj_sphere.modifiers.new(name="Boolean", type='BOOLEAN')
    mod_bool.operation = 'DIFFERENCE'
    mod_bool.solver = 'EXACT'
        
    # 2. カッター
    obj_cutters = get_or_create_mesh_object(NAME_CUTTERS, sub_coll)
    obj_cutters.display_type = 'WIRE'
    obj_cutters.hide_render = True
    obj_cutters.hide_viewport = True
    
    mod_bool.object = obj_cutters 

    # 3. 穴平面
    obj_planes = get_or_create_mesh_object(NAME_PLANES, main_coll)
    assign_material(obj_planes, mat_plane, 0)
        
    # 4. 球冠
    obj_caps = get_or_create_mesh_object(NAME_CAPS, main_coll)
    assign_material(obj_caps, mat_cap, 0)
    
    c_mod_solid = obj_caps.modifiers.get("Solidify") or obj_caps.modifiers.new(name="Solidify", type='SOLIDIFY')
    c_mod_bool = obj_caps.modifiers.get("Boolean") or obj_caps.modifiers.new(name="Boolean", type='BOOLEAN')
    c_mod_bool.operation = 'INTERSECT'
    c_mod_bool.solver = 'EXACT'
    c_mod_bool.object = obj_cutters
        
    # 5. 中心の球体
    obj_center = get_or_create_mesh_object(NAME_CENTER_SPHERES, main_coll)
    assign_material(obj_center, mat_center, 0)

    # 6. 円錐
    obj_cone = get_or_create_mesh_object(NAME_CONE, main_coll)
    assign_material(obj_cone, mat_cone, 0)

def update_meshes_and_props(context):
    props = getattr(context.scene, PROP_ID)
    
    # --- 球体 ---
    obj_sphere = bpy.data.objects.get(NAME_SPHERE)
    if obj_sphere and obj_sphere.type == 'MESH':
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=props.segments_main_u, v_segments=props.segments_main_v, radius=props.sphere_radius)
        apply_smooth_shade(bm, props.use_smooth_shade)
        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
        obj_sphere.hide_viewport = not props.show_sphere
        obj_sphere.hide_render = not props.show_sphere
        
        update_material_color(MAT_SPHERE, props.sphere_color)
        update_material_color(MAT_SPHERE_INNER, props.sphere_inner_color)
            
    # --- カッター ---
    obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
    if obj_cutters and obj_cutters.type == 'MESH':
        create_cutters_mesh(
            obj_cutters.data, props.sphere_radius, 
            props.hole_latitude, props.hole_longitude, props.hole_radius,
            props.segments_circle, props.use_smooth_shade
        )
        obj_cutters.location = props.sphere_location
        obj_cutters.rotation_euler = props.sphere_rotation

    # --- 穴平面 ---
    obj_planes = bpy.data.objects.get(NAME_PLANES)
    if obj_planes and obj_planes.type == 'MESH':
        create_planes_mesh(
            obj_planes.data, props.sphere_radius, 
            props.hole_latitude, props.hole_longitude, props.hole_radius,
            props.segments_circle, props.use_smooth_shade
        )
        obj_planes.location = props.sphere_location
        obj_planes.rotation_euler = props.sphere_rotation
        obj_planes.hide_viewport = not props.show_plane
        obj_planes.hide_render = not props.show_plane
        
        update_material_color(MAT_PLANE, props.plane_color)

    # --- 球冠(フタ) ---
    obj_caps = bpy.data.objects.get(NAME_CAPS)
    if obj_caps and obj_caps.type == 'MESH':
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=props.segments_main_u, v_segments=props.segments_main_v, radius=props.sphere_radius)
        apply_smooth_shade(bm, props.use_smooth_shade)
        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_cap
        obj_caps.hide_render = not props.show_cap
        
        update_material_color(MAT_CAP, props.cap_color)

    # --- 穴球体 (中心球体) ---
    obj_center_spheres = bpy.data.objects.get(NAME_CENTER_SPHERES)
    if obj_center_spheres and obj_center_spheres.type == 'MESH':
        create_center_spheres_mesh(
            obj_center_spheres.data, props.sphere_radius,
            props.hole_latitude, props.hole_longitude, 
            props.center_sphere_radius, props.hole_radius,
            props.segments_sub_u, props.segments_sub_v, props.use_smooth_shade,
            props.center_sphere_mode
        )
        obj_center_spheres.location = props.sphere_location
        obj_center_spheres.rotation_euler = props.sphere_rotation
        obj_center_spheres.hide_viewport = not props.show_center_sphere
        obj_center_spheres.hide_render = not props.show_center_sphere
        
        update_material_color(MAT_CENTER_SPHERE, props.center_sphere_color)
        
    # --- 中央円錐 ---
    obj_cone = bpy.data.objects.get(NAME_CONE)
    if obj_cone and obj_cone.type == 'MESH':
        bm = bmesh.new()
        bmesh.ops.create_cone(bm, cap_ends=True, segments=props.segments_circle, radius1=props.cone_radius, radius2=0, depth=props.cone_depth)
        apply_smooth_shade(bm, props.use_smooth_shade)
        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)

# ==========================================
# 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):
    
    # 品質設定
    use_smooth_shade: bpy.props.BoolProperty(name="スムーズシェード適用", default=True, update=trigger_update)
    segments_main_u: bpy.props.IntProperty(name="メイン球 分割 U", default=SEG_U_MAIN, min=3, soft_max=128, update=trigger_update)
    segments_main_v: bpy.props.IntProperty(name="メイン球 分割 V", default=SEG_V_MAIN, min=2, soft_max=64, update=trigger_update)
    segments_circle: bpy.props.IntProperty(name="穴・円錐 分割数", default=SEG_CIRCLE, min=3, soft_max=128, update=trigger_update)
    segments_sub_u: bpy.props.IntProperty(name="穴球体 分割 U", default=SEG_U_SUB, min=3, soft_max=64, update=trigger_update)
    segments_sub_v: bpy.props.IntProperty(name="穴球体 分割 V", default=SEG_V_SUB, min=2, soft_max=32, update=trigger_update)

    show_sphere: bpy.props.BoolProperty(name="球体を表示", default=True, update=trigger_update)
    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="表面色 (Aで透明度)", subtype='COLOR', default=SPHERE_COLOR, size=4, min=0.0, max=1.0, update=trigger_update)
    sphere_inner_color: bpy.props.FloatVectorProperty(name="裏面色 (Aで透明度)", subtype='COLOR', default=SPHERE_INNER_COLOR, size=4, min=0.0, max=1.0, 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_plane: bpy.props.BoolProperty(name="穴平面を表示", default=True, update=trigger_update)
    plane_color: bpy.props.FloatVectorProperty(name="穴平面色 (Aで透明度)", subtype='COLOR', default=PLANE_COLOR, size=4, min=0.0, max=1.0, update=trigger_update)

    show_cap: bpy.props.BoolProperty(name="球冠(フタ)を表示", default=True, update=trigger_update)
    cap_color: bpy.props.FloatVectorProperty(name="球冠色 (Aで透明度)", subtype='COLOR', default=CAP_COLOR, size=4, min=0.0, max=1.0, update=trigger_update)

    # 穴球体のプロパティ
    show_center_sphere: bpy.props.BoolProperty(name="穴球体を表示", default=True, update=trigger_update)
    center_sphere_mode: bpy.props.EnumProperty(
        name="表示範囲",
        items=[
            ('FULL', "全球 (両方)", "球全体を表示します"),
            ('OUTER', "外側半球のみ (凸)", "外側に膨らむ半球のみ表示します"),
            ('INNER', "内側半球のみ (凹)", "内側に凹む半球のみ表示します"),
        ],
        default='FULL',
        update=trigger_update
    )
    center_sphere_radius: bpy.props.FloatProperty(name="半径", default=CENTER_SPHERE_RADIUS, min=0.01, update=trigger_update)
    center_sphere_color: bpy.props.FloatVectorProperty(name="色 (Aで透明度)", subtype='COLOR', default=CENTER_SPHERE_COLOR, size=4, min=0.0, max=1.0, 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="色 (Aで透明度)", subtype='COLOR', default=CONE_COLOR, size=4, min=0.0, max=1.0, 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_sync_center_radius(bpy.types.Operator):
    """穴球体の半径を「穴平面の半径」に合わせます"""
    bl_idname = f"{PREFIX_NAME}.sync_center_radius"
    bl_label = "穴半径と同期"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = getattr(context.scene, PROP_ID)
        props.center_sphere_radius = props.hole_radius
        return {'FINISHED'}

class ADDON_OT_create(bpy.types.Operator):
    bl_idname = f"{PREFIX_NAME}.create"
    bl_label = "作成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        try:
            setup_base_objects(context)
            update_meshes_and_props(context)
            self.report({'INFO'}, "地球儀を生成しました。")
        except Exception as e:
            self.report({'ERROR'}, f"生成エラー: {str(e)}")
            print(f"Globe Addon Error: {e}")
        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):
        for obj_name in [NAME_SPHERE, NAME_CUTTERS, NAME_PLANES, NAME_CAPS, NAME_CENTER_SPHERES, 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.use_smooth_shade = True
            props.segments_main_u = SEG_U_MAIN
            props.segments_main_v = SEG_V_MAIN
            props.segments_circle = SEG_CIRCLE
            props.segments_sub_u = SEG_U_SUB
            props.segments_sub_v = SEG_V_SUB

            # その他の初期化
            props.show_sphere = True
            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_plane = True
            props.plane_color = PLANE_COLOR
            
            props.show_cap = True
            props.cap_color = CAP_COLOR
            
            props.show_center_sphere = True
            props.center_sphere_mode = 'FULL'
            props.center_sphere_radius = CENTER_SPHERE_RADIUS
            props.center_sphere_color = 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
            
        try:
            update_meshes_and_props(context)
        except Exception as e:
            self.report({'ERROR'}, f"エラー: {e}")
            
        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="② メッシュ品質設定", icon='MODIFIER')
        box.prop(props, "use_smooth_shade")
        row = box.row()
        row.prop(props, "segments_main_u")
        row.prop(props, "segments_main_v")
        row = box.row()
        row.prop(props, "segments_circle")
        row = box.row()
        row.prop(props, "segments_sub_u")
        row.prop(props, "segments_sub_v")
        
        # ③ 球体設定
        box = layout.box()
        box.label(text="③ 球体設定", icon='MESH_UVSPHERE')
        row = box.row()
        row.prop(props, "show_sphere", text="表示する")
        if props.show_sphere:
            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="④ 穴窓設定 (配置)", icon='OUTLINER_OB_MESH')
        box.prop(props, "hole_latitude")
        box.prop(props, "hole_longitude")
        box.prop(props, "hole_radius")
        
        # ⑤ 穴平面設定
        box = layout.box()
        box.label(text="⑤ 穴平面設定", icon='MESH_CIRCLE')
        row = box.row()
        row.prop(props, "show_plane", text="表示する")
        if props.show_plane:
            row.prop(props, "plane_color", text="色")

        # ⑥ 球冠設定
        box = layout.box()
        box.label(text="⑥ 球冠(フタ)設定", icon='MESH_ICOSPHERE')
        row = box.row()
        row.prop(props, "show_cap", text="表示する")
        if props.show_cap:
            row.prop(props, "cap_color", text="色")

        # ⑦ 穴球体設定
        box = layout.box()
        box.label(text="⑦ 穴球体 設定", icon='LIGHT_POINT')
        row = box.row()
        row.prop(props, "show_center_sphere", text="表示する")
        if props.show_center_sphere:
            box.prop(props, "center_sphere_mode")
            
            row = box.row(align=True)
            row.prop(props, "center_sphere_radius")
            # 半径同期ボタン
            row.operator(f"{PREFIX_NAME}.sync_center_radius", text="", icon='LINKED')
            
            box.prop(props, "center_sphere_color")

        # ⑧ 円錐設定
        box = layout.box()
        box.label(text="⑧ 中央の円錐設定", icon='MESH_CONE')
        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_sync_center_radius,
    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()