アルファ型 ロンドン点 エクアドル点 20260630

rapture_20260701004722.png

https://x.com/timekagura/status/2071985033481691229?s=20

bl_info = {
    "name": "Alpha Globe Generator Pro (Blender 5 Optimized)",
    "author": "Your Name",
    "version": (1, 5),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "初期値整理済。完全リアルタイム反映+確定・切り離し機能搭載。",
    "category": "Object",
}

import bpy
import math
import mathutils
import webbrowser

# =========================================================================
# 【基本設定・初期値】(1行ずつ設定可能)
# =========================================================================
TAB_NAME    = "アルファ地球儀"
PREFIX_NAME = "alpha_globe"

# 全体の位置と回転
INIT_LOC_X = 0.0
INIT_LOC_Y = 0.0
INIT_LOC_Z = 0.0
INIT_ROT_X = 0.0
INIT_ROT_Y = 0.0
INIT_ROT_Z = 0.0

# サイズとアニメーション
INIT_RADIUS = 10.0
INIT_FRAMES = 240
INIT_POINT_SIZE = 0.35
INIT_MAIN_THICK = 0.08
INIT_PEN_THICK = 0.08

# --- 経度 (縦の円) の初期値 ---
INIT_LONG_X_USE   = False
INIT_LONG_X_NUM   = 18
INIT_LONG_X_THICK = 0.05
INIT_LONG_X_C     = (1.0, 0.0, 0.0)

INIT_LONG_Y_USE   = False
INIT_LONG_Y_NUM   = 18
INIT_LONG_Y_THICK = 0.05
INIT_LONG_Y_C     = (0.0, 1.0, 0.0)

INIT_LONG_Z_USE   = True
INIT_LONG_Z_NUM   = 18
INIT_LONG_Z_THICK = 0.01   # ★経度Z 初期太さ0.01
INIT_LONG_Z_C     = (0.0, 0.7, 1.0)

# --- 緯度 (横の円) の初期値 ---
INIT_LAT_X_USE    = False
INIT_LAT_X_NUM    = 17
INIT_LAT_X_THICK  = 0.05
INIT_LAT_X_C      = (1.0, 0.0, 1.0)

INIT_LAT_Y_USE    = False
INIT_LAT_Y_NUM    = 17
INIT_LAT_Y_THICK  = 0.05
INIT_LAT_Y_C      = (0.9, 0.5, 1.0)

INIT_LAT_Z_USE    = True
INIT_LAT_Z_NUM    = 17
INIT_LAT_Z_THICK  = 0.01   # ★緯度Z 初期太さ0.01
INIT_LAT_Z_C      = (0.9, 0.8, 1.0)

# --- 基本カラー ---
INIT_C0_COLOR     = (0.60, 0.64, 0.71)
INIT_LON_COLOR    = (0.89, 0.47, 0.35)
INIT_EQ_COLOR     = (0.36, 0.79, 0.65)
INIT_TRI_COLOR    = (0.53, 0.53, 0.50)

# リンク設定
ADDON_LINKS = [
    {"label": "地球儀 緯度経度 xyz", "url": "<https://note.com/zionadmillion/n/na0b25ce45488>"},
    {"label": "地球儀 緯度経度 xyz", "url": "<https://note.com/zionadmillion/n/na0b25ce45488>"},
    {"label": "最新 posfie", "url": "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"}
]

_is_updating = False

# =========================================================================
# 【リアルタイム更新コールバック】
# =========================================================================
def update_colors(self, context):
    global _is_updating
    if _is_updating: return
    mapping = {
        "_C0_": self.c0_color, "_EQ_": self.eq_color, "_LON_": self.lon_color, "_TRI_": self.tri_color,
        "_LONG_X_": self.long_x_color, "_LONG_Y_": self.long_y_color, "_LONG_Z_": self.long_z_color,
        "_LAT_X_": self.lat_x_color, "_LAT_Y_": self.lat_y_color, "_LAT_Z_": self.lat_z_color
    }
    for mat in bpy.data.materials:
        if PREFIX_NAME in mat.name:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                for key, color in mapping.items():
                    if key in mat.name:
                        bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)

def update_speed(self, context):
    global _is_updating
    if _is_updating: return
    for obj in bpy.data.objects:
        if obj.get("globe_prefix") == PREFIX_NAME and obj.get("alpha_part") == "BASE":
            obj["frames_per_rev"] = self.frames_per_rev

def rebuild_geometry(self, context):
    global _is_updating
    if _is_updating: return
    _is_updating = True
    try:
        bases = [obj for obj in bpy.data.objects if obj.get("globe_prefix") == PREFIX_NAME and obj.get("alpha_part") == "BASE"]
        if not bases:
            _is_updating = False
            return
        for base in bases:
            coll = base.users_collection[0] if base.users_collection else context.scene.collection
            set_id = base.name.split("_")[-1]
            children = [o for o in bpy.data.objects if o.parent == base]
            for child in children:
                sub_children = [o for o in bpy.data.objects if o.parent == child]
                for sc in sub_children: bpy.data.objects.remove(sc, do_unlink=True)
                bpy.data.objects.remove(child, do_unlink=True)
            bpy.data.objects.remove(base, do_unlink=True)
            bpy.ops.mesh.alpha_globe_create_internal(set_id=set_id, coll_name=coll.name)
    finally:
        _is_updating = False

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class AlphaGlobeProperties(bpy.types.PropertyGroup):
    loc_x: bpy.props.FloatProperty(name="位置 X", default=INIT_LOC_X, update=rebuild_geometry)
    loc_y: bpy.props.FloatProperty(name="位置 Y", default=INIT_LOC_Y, update=rebuild_geometry)
    loc_z: bpy.props.FloatProperty(name="位置 Z", default=INIT_LOC_Z, update=rebuild_geometry)
    rot_x: bpy.props.FloatProperty(name="X軸", default=INIT_ROT_X, subtype='ANGLE', update=rebuild_geometry)
    rot_y: bpy.props.FloatProperty(name="Y軸", default=INIT_ROT_Y, subtype='ANGLE', update=rebuild_geometry)
    rot_z: bpy.props.FloatProperty(name="Z軸", default=INIT_ROT_Z, subtype='ANGLE', update=rebuild_geometry)
    
    radius: bpy.props.FloatProperty(name="基準半径 (R)", default=INIT_RADIUS, min=0.1, update=rebuild_geometry)
    frames_per_rev: bpy.props.IntProperty(name="1周のフレーム数", default=INIT_FRAMES, min=1, update=update_speed)
    point_size: bpy.props.FloatProperty(name="点のサイズ", default=INIT_POINT_SIZE, min=0.01, update=rebuild_geometry)
    
    main_line_thick: bpy.props.FloatProperty(name="赤道・ロンドン緯線の太さ", default=INIT_MAIN_THICK, min=0.001, update=rebuild_geometry)
    pen_line_thick: bpy.props.FloatProperty(name="ペン線の太さ (三角形)", default=INIT_PEN_THICK, min=0.001, update=rebuild_geometry)
    
    long_x_use: bpy.props.BoolProperty(default=INIT_LONG_X_USE, update=rebuild_geometry)
    long_x_num: bpy.props.IntProperty(default=INIT_LONG_X_NUM, min=1, update=rebuild_geometry)
    long_x_thick: bpy.props.FloatProperty(default=INIT_LONG_X_THICK, min=0.001, update=rebuild_geometry)
    long_x_color: bpy.props.FloatVectorProperty(subtype='COLOR', default=INIT_LONG_X_C, min=0.0, max=1.0, update=update_colors)
    
    long_y_use: bpy.props.BoolProperty(default=INIT_LONG_Y_USE, update=rebuild_geometry)
    long_y_num: bpy.props.IntProperty(default=INIT_LONG_Y_NUM, min=1, update=rebuild_geometry)
    long_y_thick: bpy.props.FloatProperty(default=INIT_LONG_Y_THICK, min=0.001, update=rebuild_geometry)
    long_y_color: bpy.props.FloatVectorProperty(subtype='COLOR', default=INIT_LONG_Y_C, min=0.0, max=1.0, update=update_colors)
    
    long_z_use: bpy.props.BoolProperty(default=INIT_LONG_Z_USE, update=rebuild_geometry)
    long_z_num: bpy.props.IntProperty(default=INIT_LONG_Z_NUM, min=1, update=rebuild_geometry)
    long_z_thick: bpy.props.FloatProperty(default=INIT_LONG_Z_THICK, min=0.001, update=rebuild_geometry)
    long_z_color: bpy.props.FloatVectorProperty(subtype='COLOR', default=INIT_LONG_Z_C, min=0.0, max=1.0, update=update_colors)

    lat_x_use: bpy.props.BoolProperty(default=INIT_LAT_X_USE, update=rebuild_geometry)
    lat_x_num: bpy.props.IntProperty(default=INIT_LAT_X_NUM, min=1, update=rebuild_geometry)
    lat_x_thick: bpy.props.FloatProperty(default=INIT_LAT_X_THICK, min=0.001, update=rebuild_geometry)
    lat_x_color: bpy.props.FloatVectorProperty(subtype='COLOR', default=INIT_LAT_X_C, min=0.0, max=1.0, update=update_colors)
    
    lat_y_use: bpy.props.BoolProperty(default=INIT_LAT_Y_USE, update=rebuild_geometry)
    lat_y_num: bpy.props.IntProperty(default=INIT_LAT_Y_NUM, min=1, update=rebuild_geometry)
    lat_y_thick: bpy.props.FloatProperty(default=INIT_LAT_Y_THICK, min=0.001, update=rebuild_geometry)
    lat_y_color: bpy.props.FloatVectorProperty(subtype='COLOR', default=INIT_LAT_Y_C, min=0.0, max=1.0, update=update_colors)
    
    lat_z_use: bpy.props.BoolProperty(default=INIT_LAT_Z_USE, update=rebuild_geometry)
    lat_z_num: bpy.props.IntProperty(default=INIT_LAT_Z_NUM, min=1, update=rebuild_geometry)
    lat_z_thick: bpy.props.FloatProperty(default=INIT_LAT_Z_THICK, min=0.001, update=rebuild_geometry)
    lat_z_color: bpy.props.FloatVectorProperty(subtype='COLOR', default=INIT_LAT_Z_C, min=0.0, max=1.0, update=update_colors)

    c0_color: bpy.props.FloatVectorProperty(name="C0色", subtype='COLOR', default=INIT_C0_COLOR, min=0.0, max=1.0, update=update_colors)
    eq_color: bpy.props.FloatVectorProperty(name="赤道色", subtype='COLOR', default=INIT_EQ_COLOR, min=0.0, max=1.0, update=update_colors)
    lon_color: bpy.props.FloatVectorProperty(name="ロンドン色", subtype='COLOR', default=INIT_LON_COLOR, min=0.0, max=1.0, update=update_colors)
    tri_color: bpy.props.FloatVectorProperty(name="線分色", subtype='COLOR', default=INIT_TRI_COLOR, min=0.0, max=1.0, update=update_colors)

# =========================================================================
# 【内部生成エンジン】
# =========================================================================
class ALPHA_OT_create_globe_internal(bpy.types.Operator):
    bl_idname = "mesh.alpha_globe_create_internal"
    bl_label = "内部生成処理"
    bl_options = {'INTERNAL'}
    
    set_id: bpy.props.StringProperty(default="1")
    coll_name: bpy.props.StringProperty(default="")

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        coll = bpy.data.collections.get(self.coll_name)
        if not coll:
            coll = bpy.data.collections.new(f"アルファ地球儀_{self.set_id}")
            context.scene.collection.children.link(coll)

        def move_to_coll(obj):
            for c in obj.users_collection: c.objects.unlink(obj)
            coll.objects.link(obj)

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

        sid = self.set_id
        mats = {
            "C0": get_or_create_mat(f"{PREFIX_NAME}_C0_{sid}", props.c0_color),
            "EQ": get_or_create_mat(f"{PREFIX_NAME}_EQ_{sid}", props.eq_color),
            "LON": get_or_create_mat(f"{PREFIX_NAME}_LON_{sid}", props.lon_color),
            "TRI": get_or_create_mat(f"{PREFIX_NAME}_TRI_{sid}", props.tri_color),
            "LONG_X": get_or_create_mat(f"{PREFIX_NAME}_LONG_X_{sid}", props.long_x_color),
            "LONG_Y": get_or_create_mat(f"{PREFIX_NAME}_LONG_Y_{sid}", props.long_y_color),
            "LONG_Z": get_or_create_mat(f"{PREFIX_NAME}_LONG_Z_{sid}", props.long_z_color),
            "LAT_X": get_or_create_mat(f"{PREFIX_NAME}_LAT_X_{sid}", props.lat_x_color),
            "LAT_Y": get_or_create_mat(f"{PREFIX_NAME}_LAT_Y_{sid}", props.lat_y_color),
            "LAT_Z": get_or_create_mat(f"{PREFIX_NAME}_LAT_Z_{sid}", props.lat_z_color),
        }

        R, t, p, pen_t = props.radius, props.main_line_thick, props.point_size, props.pen_line_thick
        
        bpy.ops.object.empty_add(type='ARROWS', radius=R*1.2, location=(0,0,0))
        base = context.active_object
        base.name = f"AlphaGlobe_Base_{sid}"
        base["globe_prefix"], base["alpha_part"] = PREFIX_NAME, "BASE"
        base["frames_per_rev"] = props.frames_per_rev 
        move_to_coll(base)

        def create_curve_ring(name, part, rad, thick, loc, rot, mat):
            bpy.ops.curve.primitive_bezier_circle_add(radius=rad, location=loc, rotation=rot)
            obj = context.active_object
            obj.name = name
            obj.data.bevel_depth = thick / 2.0
            obj.data.bevel_resolution = 4
            obj.data.materials.append(mat)
            obj.parent = base
            obj["globe_prefix"], obj["alpha_part"] = PREFIX_NAME, part
            move_to_coll(obj)

        config = [
            ('LONG_X', props.long_x_use, props.long_x_num, props.long_x_thick),
            ('LONG_Y', props.long_y_use, props.long_y_num, props.long_y_thick),
            ('LONG_Z', props.long_z_use, props.long_z_num, props.long_z_thick),
            ('LAT_X',  props.lat_x_use,  props.lat_x_num,  props.lat_x_thick),
            ('LAT_Y',  props.lat_y_use,  props.lat_y_num,  props.lat_y_thick),
            ('LAT_Z',  props.lat_z_use,  props.lat_z_num,  props.lat_z_thick)
        ]
        for part, use, num, thick in config:
            if not use: continue
            mat = mats[part]
            if part.startswith('LONG'):
                for i in range(num):
                    angle = i * (math.pi / num)
                    if part == 'LONG_X': rot = (angle, 0.0, 0.0)
                    elif part == 'LONG_Y': rot = (0.0, angle, 0.0)
                    else: rot = (math.pi/2.0, 0.0, angle)
                    create_curve_ring(f"{part}_{i+1}_{sid}", part, R, thick, (0,0,0), rot, mat)
            else:
                step = math.pi / (num + 1)
                for i in range(1, num + 1):
                    theta = -math.pi/2.0 + i * step
                    r_val = max(0.001, R * math.cos(theta))
                    z_val = R * math.sin(theta)
                    if part == 'LAT_X': loc, rot = (z_val, 0, 0), (0, math.pi/2.0, 0)
                    elif part == 'LAT_Y': loc, rot = (0, z_val, 0), (math.pi/2.0, 0, 0)
                    else: loc, rot = (0, 0, z_val), (0, 0, 0)
                    create_curve_ring(f"{part}_{i}_{sid}", part, r_val, thick, loc, rot, mat)

        lat_lon_rad, lon_eq_rad = math.radians(51.5), math.radians(-78.5)
        r_lon, z_lon = R * math.cos(lat_lon_rad), R * math.sin(lat_lon_rad)
        create_curve_ring(f"Equator_Line_{sid}", "EQ", R, t, (0,0,0), (0,0,0), mats["EQ"])
        create_curve_ring(f"London_Line_{sid}", "LON", r_lon, t, (0,0,z_lon), (0,0,0), mats["LON"])

        bpy.ops.mesh.primitive_uv_sphere_add(radius=p * 1.2, segments=32, ring_count=16, location=(0,0,0))
        c0 = context.active_object
        c0.name, c0.parent, c0["globe_prefix"], c0["alpha_part"] = f"C0_Center_{sid}", base, PREFIX_NAME, "C0"
        c0.data.materials.append(mats["C0"]); move_to_coll(c0)

        bpy.ops.object.empty_add(type='PLAIN_AXES', radius=R*1.5, location=(0,0,0))
        rotor = context.active_object
        rotor.name, rotor.parent, rotor["globe_prefix"], rotor["alpha_part"] = f"Rotor_Anim_{sid}", base, PREFIX_NAME, "ROTOR"
        move_to_coll(rotor)
        
        fcurve = rotor.driver_add('rotation_euler', 2)
        driver = fcurve.driver
        driver.type = 'SCRIPTED'
        var = driver.variables.new()
        var.name = "spd"
        var.type = 'SINGLE_PROP'
        target = var.targets[0]
        target.id_type = 'OBJECT'
        target.id = base
        target.data_path = '["frames_per_rev"]'
        driver.expression = "(frame / spd) * 6.2831853"

        x_eq, y_eq = R * math.cos(lon_eq_rad), R * math.sin(lon_eq_rad)
        bpy.ops.mesh.primitive_uv_sphere_add(radius=p, segments=32, ring_count=16, location=(x_eq, y_eq, 0))
        eq_pt = context.active_object
        eq_pt.name, eq_pt.parent, eq_pt["globe_prefix"], eq_pt["alpha_part"] = f"Equator_Point_{sid}", rotor, PREFIX_NAME, "EQ"
        eq_pt.data.materials.append(mats["EQ"]); move_to_coll(eq_pt)

        bpy.ops.mesh.primitive_uv_sphere_add(radius=p, segments=32, ring_count=16, location=(r_lon, 0, z_lon))
        lon_pt = context.active_object
        lon_pt.name, lon_pt.parent, lon_pt["globe_prefix"], lon_pt["alpha_part"] = f"London_Point_{sid}", rotor, PREFIX_NAME, "LON"
        lon_pt.data.materials.append(mats["LON"]); move_to_coll(lon_pt)

        curve_data = bpy.data.curves.new(f"TriangleCurve_{sid}", type='CURVE')
        curve_data.dimensions = '3D'
        curve_data.fill_mode = 'FULL'
        curve_data.bevel_depth = pen_t / 2.0 
        curve_data.bevel_resolution = 4
        spline = curve_data.splines.new('POLY')
        spline.points.add(2)
        spline.points[0].co = (0.0, 0.0, 0.0, 1.0)
        spline.points[1].co = (r_lon, 0.0, z_lon, 1.0)
        spline.points[2].co = (x_eq, y_eq, 0.0, 1.0)
        spline.use_cyclic_u = True 
        tri_obj = bpy.data.objects.new(f"Triangle_Edges_{sid}", curve_data)
        tri_obj.data.materials.append(mats["TRI"])
        tri_obj.parent, tri_obj["globe_prefix"], tri_obj["alpha_part"] = rotor, PREFIX_NAME, "TRIANGLE"
        coll.objects.link(tri_obj)

        rot_mat = mathutils.Euler((props.rot_x, props.rot_y, props.rot_z), 'XYZ').to_matrix().to_4x4()
        loc_mat = mathutils.Matrix.Translation((props.loc_x, props.loc_y, props.loc_z))
        base.matrix_world = loc_mat @ rot_mat @ base.matrix_world

        context.view_layer.objects.active = base
        base.select_set(True)
        return {'FINISHED'}

class ALPHA_OT_create_globe(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create"
    bl_label = "アルファ地球儀を新規生成"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        set_count = 1
        while bpy.data.collections.get(f"アルファ地球儀_{set_count}"): set_count += 1
        bpy.ops.mesh.alpha_globe_create_internal(set_id=str(set_count), coll_name=f"アルファ地球儀_{set_count}")
        return {'FINISHED'}

# =========================================================================
# 【★新規追加★ 切り離し(確定)オペレーター】
# =========================================================================
class ALPHA_OT_detach_globe(bpy.types.Operator):
    """現在編集中のモデルをアドオンの管理下から切り離し、独立させます。
       これにより、以降パラメータを変更してもこのモデルは変形・影響を受けなくなります。"""
    bl_idname = f"object.{PREFIX_NAME.lower()}_detach"
    bl_label = "現在のモデルを確定して切り離す"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        count = 0
        # シーン内のオブジェクトからアドオン識別タグを削除
        for obj in context.scene.objects:
            if obj.get("globe_prefix") == PREFIX_NAME:
                del obj["globe_prefix"]
                if "alpha_part" in obj:
                    del obj["alpha_part"]
                count += 1
        
        # マテリアル名からPREFIXを消し、色変更の影響を受けないようにする
        for mat in bpy.data.materials:
            if PREFIX_NAME in mat.name:
                mat.name = mat.name.replace(PREFIX_NAME, "Detached_Globe")
                
        self.report({'INFO'}, f"モデルを確定しました。以後の数値変更の影響を受けません。")
        return {'FINISHED'}

# =========================================================================
# 【UIパネル】
# =========================================================================
class ALPHA_OT_toggle_visibility(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_toggle_vis"
    bl_label = "表示切替"
    part_type: bpy.props.StringProperty() 
    def execute(self, context):
        objs = [o for o in context.scene.objects if o.get("globe_prefix") == PREFIX_NAME and o.get("alpha_part") == self.part_type]
        if not objs: return {'CANCELLED'}
        state = not objs[0].hide_viewport
        for o in objs: o.hide_viewport = state
        return {'FINISHED'}

class ALPHA_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_main_panel"
    bl_label = "アルファ地球儀 (生成・基本)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        box_gen = layout.box()
        box_gen.scale_y = 1.3
        box_gen.operator(f"mesh.{PREFIX_NAME.lower()}_create", text="新しく地球儀を生成", icon='PLUS')
        
        # ★ 追加: 切り離しボタン
        box_gen.operator(f"object.{PREFIX_NAME.lower()}_detach", text="現在のモデルを確定して切り離す", icon='UNLINKED')

        box_b = layout.box()
        box_b.label(text="基本設定 (リアルタイム反映)", icon='PREFERENCES')
        col = box_b.column(align=True)
        col.prop(props, "radius")
        col.prop(props, "frames_per_rev")
        col.prop(props, "point_size")
        
        box_t = layout.box()
        box_t.label(text="太さの調整", icon='IPO_CONSTANT')
        col_t = box_t.column(align=True)
        col_t.prop(props, "main_line_thick")
        col_t.prop(props, "pen_line_thick")

        box_p = layout.box()
        box_p.label(text="アニメーションパーツ表示", icon='ANIM')
        for title, icon, p_col, p_type in [("C0 (中心点)", 'DOT', "c0_color", 'C0'),
                                           ("赤道 (緯線・点)", 'MESH_TORUS', "eq_color", 'EQ'),
                                           ("ロンドン (緯線・点)", 'MESH_TORUS', "lon_color", 'LON'),
                                           ("ペン線 (三角形)", 'MESH_DATA', "tri_color", 'TRIANGLE')]:
            r = box_p.row(align=True)
            r.prop(props, p_col, text="")
            r.operator(f"object.{PREFIX_NAME.lower()}_toggle_vis", text=title, icon=icon).part_type = p_type

class ALPHA_PT_circles_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_circles_panel"
    bl_label = "6種類の円の設定 (リアルタイム反映)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        def draw_ring(parent, title, prefix):
            c = parent.column(align=True)
            c.label(text=title)
            r = c.row(align=True)
            r.prop(props, f"{prefix}_use", text="")
            r.prop(props, f"{prefix}_color", text="")
            c.prop(props, f"{prefix}_num", text="数(Size)")
            c.prop(props, f"{prefix}_thick", text="太さ")
            c.operator(f"object.{PREFIX_NAME.lower()}_toggle_vis", text="表示切替").part_type = prefix.upper()

        b1 = self.layout.box()
        b1.label(text="経度 (縦の円)", icon='MESH_TORUS')
        r1 = b1.row(align=True)
        draw_ring(r1, "経度 X", "long_x"); draw_ring(r1, "経度 Y", "long_y"); draw_ring(r1, "経度 Z", "long_z")

        b2 = self.layout.box()
        b2.label(text="緯度 (横の円)", icon='MESH_TORUS')
        r2 = b2.row(align=True)
        draw_ring(r2, "緯度 X", "lat_x"); draw_ring(r2, "緯度 Y", "lat_y"); draw_ring(r2, "緯度 Z", "lat_z")

class ALPHA_PT_transform_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_transform_panel"
    bl_label = "全体の位置・回転"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        c1, c2 = self.layout.column(align=True), self.layout.column(align=True)
        c1.label(text="位置:"); c1.prop(props, "loc_x"); c1.prop(props, "loc_y"); c1.prop(props, "loc_z")
        c2.label(text="回転:"); c2.prop(props, "rot_x"); c2.prop(props, "rot_y"); c2.prop(props, "rot_z")

class ALPHA_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_NAME.lower()}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

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

class ALPHA_PT_footer_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_footer_panel"
    bl_label = "リンク / 管理"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_order = 100
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        for link in ADDON_LINKS: layout.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text=link["label"], icon='URL').url = link["url"]
        layout.separator()
        layout.operator(f"wm.{PREFIX_NAME.lower()}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

classes = [
    AlphaGlobeProperties, ALPHA_OT_create_globe_internal, ALPHA_OT_create_globe, 
    ALPHA_OT_detach_globe, ALPHA_OT_toggle_visibility, # Detachオペレーター追加
    ALPHA_OT_open_url, ALPHA_OT_remove_addon,
    ALPHA_PT_main_panel, ALPHA_PT_circles_panel, ALPHA_PT_transform_panel, ALPHA_PT_footer_panel
]
def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, f"{PREFIX_NAME.lower()}_props", bpy.props.PointerProperty(type=AlphaGlobeProperties))
def unregister():
    if hasattr(bpy.types.Scene, f"{PREFIX_NAME.lower()}_props"): delattr(bpy.types.Scene, f"{PREFIX_NAME.lower()}_props")
    for c in reversed(classes):
        try: bpy.utils.unregister_class(c)
        except: pass
if __name__ == "__main__":
    try: unregister()
    except: pass
    register()