骨格 立方体作成リスト

パネル追加で 矢印を作る 矢印長さは 円錐先端から 円柱後端までの 全長
この矢印が 立方体 空間内を通り 球体表面の1点を通過し
球体中心点に 届く 
立方体と 球体の最新値を更新ボタンで取得し
矢印長さの中間点が 立方体表面 に 位置する
この立方体表面を 移動する矢印中間点を
UV座標で 操作したい

rapture_20260818234741.png

# Code Created : 2024_0526_1600

bl_info = {
    "name": "Skeleton Cube Generator (Pro)",
    "author": "Your Name",
    "version": (2, 6, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar",
    "description": "Generates a skeletal cube, sphere, and an interactive UV Arrow with display toggles.",
    "category": "3D View",
}

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

# =========================================================
# 共通定義
# =========================================================
ADDON_TITLE = "Skeleton Cube"
PREFIX_VAL = "SKELETON_CUBE"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Skeleton Cube"

# =========================================================
# ユーザー設定パラメータ (初期値)
# =========================================================
TARGET_COLLECTION_NAME = "SkeletonCube_Collection"

DEFAULT_CENTER_X, DEFAULT_CENTER_Y, DEFAULT_CENTER_Z = 0.0, 0.0, 0.0
DEFAULT_ROTATION_X, DEFAULT_ROTATION_Y, DEFAULT_ROTATION_Z = 0.0, 0.0, 0.0
DEFAULT_EDGE_LENGTH = 2.0
DEFAULT_CYLINDER_RADIUS = 0.05
DEFAULT_COLOR_R, DEFAULT_COLOR_G, DEFAULT_COLOR_B = 0.2, 0.6, 1.0
DEFAULT_ALPHA = 1.0

DEFAULT_VERT_SPHERES_SHOW = True
DEFAULT_VERT_SPHERE_RADIUS = 0.08
DEFAULT_VERT_SPH_COLOR_R, DEFAULT_VERT_SPH_COLOR_G, DEFAULT_VERT_SPH_COLOR_B = 1.0, 0.3, 0.3
DEFAULT_VERT_SPH_ALPHA = 1.0

DEFAULT_SINGLE_SPHERE_SHOW = True
DEFAULT_SINGLE_SPHERE_LOC_X, DEFAULT_SINGLE_SPHERE_LOC_Y, DEFAULT_SINGLE_SPHERE_LOC_Z = 0.0, 0.0, 0.0
DEFAULT_SINGLE_SPHERE_RADIUS = 0.5
DEFAULT_SINGLE_SPH_COLOR_R, DEFAULT_SINGLE_SPH_COLOR_G, DEFAULT_SINGLE_SPH_COLOR_B = 0.2, 1.0, 0.2
DEFAULT_SINGLE_SPH_ALPHA = 1.0

DEFAULT_ARROW_SHOW = True
DEFAULT_ARROW_U = 0.5
DEFAULT_ARROW_V = 0.5
DEFAULT_ARROW_AUTO_LEN = True
DEFAULT_ARROW_LENGTH = 3.0
DEFAULT_ARROW_RADIUS = 0.02
DEFAULT_ARROW_CONE_LEN = 0.2
DEFAULT_ARROW_COLOR_R, DEFAULT_ARROW_COLOR_G, DEFAULT_ARROW_COLOR_B = 1.0, 0.8, 0.1
DEFAULT_ARROW_ALPHA = 1.0

# =========================================================
# リンク・補助関数
# =========================================================
def link_to_collection(context, obj, coll_name):
    coll = bpy.data.collections.get(coll_name)
    if not coll:
        coll = bpy.data.collections.new(coll_name)
        context.scene.collection.children.link(coll)
    if obj.name not in coll.objects:
        coll.objects.link(obj)
    for c in obj.users_collection:
        if c != coll:
            c.objects.unlink(obj)

def setup_material(mat_name, color, alpha):
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(mat_name)
        mat.use_nodes = True
        
    mat.use_backface_culling = False
    mat.diffuse_color = (color[0], color[1], color[2], alpha)
    
    if hasattr(mat, "blend_method"): mat.blend_method = 'BLEND'
    if hasattr(mat, "shadow_method"): mat.shadow_method = 'NONE'

    nodes = mat.node_tree.nodes
    bsdf = nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs['Base Color'].default_value = (color[0], color[1], color[2], alpha)
        if 'Alpha' in bsdf.inputs:
            bsdf.inputs['Alpha'].default_value = alpha
    return mat

# =========================================================
# メイン生成・更新ロジック
# =========================================================
def update_skeleton_cube(context):
    props = context.scene.skeleton_cube_props
    obj_name = PREFIX_VAL
    mesh_name = f"{PREFIX_VAL}_MESH"
    
    mesh = bpy.data.meshes.get(mesh_name)
    if not mesh: mesh = bpy.data.meshes.new(mesh_name)
    else: mesh.clear_geometry()
        
    obj = bpy.data.objects.get(obj_name)
    if not obj: obj = bpy.data.objects.new(obj_name, mesh)
        
    link_to_collection(context, obj, props.target_collection_name)
        
    bm = bmesh.new()
    L = props.edge_length
    h = L / 2.0
    r_cyl = props.cylinder_radius
    
    verts_pos = [
        Vector((-h, -h, -h)), Vector((+h, -h, -h)), Vector((+h, +h, -h)), Vector((-h, +h, -h)),
        Vector((-h, -h, +h)), Vector((+h, -h, +h)), Vector((+h, +h, +h)), Vector((-h, +h, +h))
    ]
    edges = [(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
    
    for i, j in edges:
        start, end = verts_pos[i], verts_pos[j]
        center = (start + end) / 2.0
        direction = (end - start).normalized()
        length = (start - end).length
        rot_quat = direction.to_track_quat('Z', 'Y')
        matrix = Matrix.Translation(center) @ rot_quat.to_matrix().to_4x4()
        bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, radius1=r_cyl, radius2=r_cyl, depth=length, matrix=matrix)
        
    if props.show_vert_spheres:
        r_sph = props.vert_sphere_radius
        for v in verts_pos:
            start_face_count = len(bm.faces)
            bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=16, radius=r_sph, matrix=Matrix.Translation(v))
            bm.faces.ensure_lookup_table() 
            for face_idx in range(start_face_count, len(bm.faces)):
                bm.faces[face_idx].material_index = 1
                
    bm.to_mesh(mesh)
    bm.free()
    mesh.update()
    
    obj.location = props.loc
    obj.rotation_euler = props.rot
    
    mat_cyl = setup_material(f"{PREFIX_VAL}_CYL_MAT", props.color, props.alpha)
    mat_vert_sph = setup_material(f"{PREFIX_VAL}_VERT_SPH_MAT", props.vert_sphere_color, props.vert_sphere_alpha)
    
    obj.data.materials.clear()
    obj.data.materials.append(mat_cyl)
    obj.data.materials.append(mat_vert_sph)

def update_single_sphere(context):
    props = context.scene.skeleton_cube_props
    obj_name = f"{PREFIX_VAL}_SINGLE_SPH"
    mesh_name = f"{obj_name}_MESH"
    
    mesh = bpy.data.meshes.get(mesh_name)
    if not mesh: mesh = bpy.data.meshes.new(mesh_name)
    else: mesh.clear_geometry()
        
    obj = bpy.data.objects.get(obj_name)
    if not obj: obj = bpy.data.objects.new(obj_name, mesh)
        
    link_to_collection(context, obj, props.target_collection_name)
    
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=32, radius=props.single_sphere_radius)
    bm.to_mesh(mesh)
    bm.free()
    mesh.update()
    
    cube_obj = bpy.data.objects.get(PREFIX_VAL)
    if cube_obj:
        obj.parent = cube_obj
        obj.location = Vector(props.single_sphere_loc)
        obj.rotation_euler = (0, 0, 0)
    else:
        obj.parent = None
        obj.location = Vector(props.single_sphere_loc)
        obj.rotation_euler = (0, 0, 0)

    mat = setup_material(f"{obj_name}_MAT", props.single_sphere_color, props.single_sphere_alpha)
    obj.data.materials.clear()
    obj.data.materials.append(mat)

def update_arrow(context):
    props = context.scene.skeleton_cube_props
    obj_name = f"{PREFIX_VAL}_ARROW"
    mesh_name = f"{obj_name}_MESH"
    
    mesh = bpy.data.meshes.get(mesh_name)
    if not mesh: mesh = bpy.data.meshes.new(mesh_name)
    else: mesh.clear_geometry()
        
    obj = bpy.data.objects.get(obj_name)
    if not obj: obj = bpy.data.objects.new(obj_name, mesh)
        
    link_to_collection(context, obj, props.target_collection_name)
    
    if not props.show_arrow:
        mesh.update()
        return

    # 1. UV座標から立方体表面の中間点(M)を計算
    theta = props.arrow_u * 2.0 * math.pi
    phi = (props.arrow_v - 0.5) * math.pi
    
    vx = math.cos(phi) * math.cos(theta)
    vy = math.cos(phi) * math.sin(theta)
    vz = math.sin(phi)
    
    half_L = props.edge_length / 2.0
    tx = abs(half_L / vx) if abs(vx) > 1e-6 else float('inf')
    ty = abs(half_L / vy) if abs(vy) > 1e-6 else float('inf')
    tz = abs(half_L / vz) if abs(vz) > 1e-6 else float('inf')
    t = min(tx, ty, tz)
    
    M_loc = Vector((vx * t, vy * t, vz * t))
    
    # 2. ワールド座標への変換
    rot_mat = Euler(props.rot, 'XYZ').to_matrix()
    M_world = Vector(props.loc) + rot_mat @ M_loc
    C_sph = Vector(props.loc) + rot_mat @ Vector(props.single_sphere_loc)
    
    # 3. 方向と距離の計算
    direction = C_sph - M_world
    dist = direction.length
    if dist < 1e-4:
        mesh.update()
        return
        
    direction.normalize()
    
    # 4. 全長の決定
    if props.arrow_auto_length:
        L_arrow = dist * 2.0  # 球体中心に届く長さ
    else:
        L_arrow = props.arrow_length

    L_cone = props.arrow_cone_len
    if L_cone > L_arrow: L_cone = L_arrow
    L_cyl = L_arrow - L_cone
    
    # 先端と後端の計算(中間点 M_world を中心にする)
    P_start = M_world - direction * (L_arrow / 2.0)
    P_cone_base = M_world + direction * (L_arrow / 2.0) - direction * L_cone
    
    C_cyl = (P_start + P_cone_base) / 2.0
    C_cone = P_cone_base + direction * (L_cone / 2.0)
    
    rot_quat = direction.to_track_quat('Z', 'Y')
    mat_rot = rot_quat.to_matrix().to_4x4()
    
    bm = bmesh.new()
    
    if L_cyl > 0:
        matrix_cyl = Matrix.Translation(C_cyl) @ mat_rot
        bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, 
                              radius1=props.arrow_radius, radius2=props.arrow_radius, 
                              depth=L_cyl, matrix=matrix_cyl)
    if L_cone > 0:
        matrix_cone = Matrix.Translation(C_cone) @ mat_rot
        bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, 
                              radius1=props.arrow_radius * 2.0, radius2=0, 
                              depth=L_cone, matrix=matrix_cone)

    bm.to_mesh(mesh)
    bm.free()
    mesh.update()
    
    mat = setup_material(f"{obj_name}_MAT", props.arrow_color, props.arrow_alpha)
    obj.data.materials.clear()
    obj.data.materials.append(mat)

# =========================================================
# プロパティ
# =========================================================
def on_property_update(self, context):
    if self.cube_created: update_skeleton_cube(context)
    if self.single_created: update_single_sphere(context)
    if self.arrow_created: update_arrow(context)
    for area in context.screen.areas:
        if area.type == 'VIEW_3D': area.tag_redraw()

def on_view_update(self, context):
    obj_cube = bpy.data.objects.get(PREFIX_VAL)
    if obj_cube:
        obj_cube.hide_viewport = not self.view_show_cube
        obj_cube.hide_render = not self.view_show_cube

    obj_single = bpy.data.objects.get(f"{PREFIX_VAL}_SINGLE_SPH")
    if obj_single:
        obj_single.hide_viewport = not self.view_show_single
        obj_single.hide_render = not self.view_show_single

    obj_arrow = bpy.data.objects.get(f"{PREFIX_VAL}_ARROW")
    if obj_arrow:
        obj_arrow.hide_viewport = not self.view_show_arrow
        obj_arrow.hide_render = not self.view_show_arrow

class SkeletonCubeProperties(bpy.types.PropertyGroup):
    target_collection_name: bpy.props.StringProperty(name="保存コレクション名", default=TARGET_COLLECTION_NAME)
    cube_created: bpy.props.BoolProperty(default=False)
    single_created: bpy.props.BoolProperty(default=False)
    arrow_created: bpy.props.BoolProperty(default=False)
    
    # 表示・非表示トグル用
    view_show_cube: bpy.props.BoolProperty(name="表示", default=True, update=on_view_update)
    view_show_single: bpy.props.BoolProperty(name="表示", default=True, update=on_view_update)
    view_show_arrow: bpy.props.BoolProperty(name="表示", default=True, update=on_view_update)

    loc: bpy.props.FloatVectorProperty(name="位置", size=3, default=(DEFAULT_CENTER_X, DEFAULT_CENTER_Y, DEFAULT_CENTER_Z), update=on_property_update)
    rot: bpy.props.FloatVectorProperty(name="回転", size=3, subtype='EULER', default=(math.radians(DEFAULT_ROTATION_X), math.radians(DEFAULT_ROTATION_Y), math.radians(DEFAULT_ROTATION_Z)), update=on_property_update)
    edge_length: bpy.props.FloatProperty(name="立方体1辺", default=DEFAULT_EDGE_LENGTH, min=0.001, update=on_property_update)
    cylinder_radius: bpy.props.FloatProperty(name="円柱半径", default=DEFAULT_CYLINDER_RADIUS, min=0.001, update=on_property_update)
    color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(DEFAULT_COLOR_R, DEFAULT_COLOR_G, DEFAULT_COLOR_B), update=on_property_update)
    alpha: bpy.props.FloatProperty(name="透明度", default=DEFAULT_ALPHA, min=0.0, max=1.0, update=on_property_update)
    
    show_vert_spheres: bpy.props.BoolProperty(name="頂点に球体を表示", default=DEFAULT_VERT_SPHERES_SHOW, update=on_property_update)
    vert_sphere_radius: bpy.props.FloatProperty(name="球体半径", default=DEFAULT_VERT_SPHERE_RADIUS, min=0.001, update=on_property_update)
    vert_sphere_color: bpy.props.FloatVectorProperty(name="球体色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(DEFAULT_VERT_SPH_COLOR_R, DEFAULT_VERT_SPH_COLOR_G, DEFAULT_VERT_SPH_COLOR_B), update=on_property_update)
    vert_sphere_alpha: bpy.props.FloatProperty(name="球体透明度", default=DEFAULT_VERT_SPH_ALPHA, min=0.0, max=1.0, update=on_property_update)

    show_single_sphere: bpy.props.BoolProperty(name="単独球体を生成する", default=DEFAULT_SINGLE_SPHERE_SHOW, update=on_property_update)
    single_sphere_loc: bpy.props.FloatVectorProperty(name="相対位置", size=3, default=(DEFAULT_SINGLE_SPHERE_LOC_X, DEFAULT_SINGLE_SPHERE_LOC_Y, DEFAULT_SINGLE_SPHERE_LOC_Z), update=on_property_update)
    single_sphere_radius: bpy.props.FloatProperty(name="球体の半径", default=DEFAULT_SINGLE_SPHERE_RADIUS, min=0.001, update=on_property_update)
    single_sphere_color: bpy.props.FloatVectorProperty(name="球体の色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(DEFAULT_SINGLE_SPH_COLOR_R, DEFAULT_SINGLE_SPH_COLOR_G, DEFAULT_SINGLE_SPH_COLOR_B), update=on_property_update)
    single_sphere_alpha: bpy.props.FloatProperty(name="球体の透明度", default=DEFAULT_SINGLE_SPH_ALPHA, min=0.0, max=1.0, update=on_property_update)

    show_arrow: bpy.props.BoolProperty(name="矢印を表示する", default=DEFAULT_ARROW_SHOW, update=on_property_update)
    arrow_u: bpy.props.FloatProperty(name="U (横周り)", min=0.0, max=1.0, default=DEFAULT_ARROW_U, update=on_property_update)
    arrow_v: bpy.props.FloatProperty(name="V (上下)", min=0.0, max=1.0, default=DEFAULT_ARROW_V, update=on_property_update)
    arrow_auto_length: bpy.props.BoolProperty(name="球体に届く長さに自動調整", default=DEFAULT_ARROW_AUTO_LEN, update=on_property_update)
    arrow_length: bpy.props.FloatProperty(name="全体の長さ", min=0.001, default=DEFAULT_ARROW_LENGTH, update=on_property_update)
    arrow_radius: bpy.props.FloatProperty(name="矢印の太さ", min=0.001, default=DEFAULT_ARROW_RADIUS, update=on_property_update)
    arrow_cone_len: bpy.props.FloatProperty(name="先端の長さ", min=0.001, default=DEFAULT_ARROW_CONE_LEN, update=on_property_update)
    arrow_color: bpy.props.FloatVectorProperty(name="矢印の色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(DEFAULT_ARROW_COLOR_R, DEFAULT_ARROW_COLOR_G, DEFAULT_ARROW_COLOR_B), update=on_property_update)
    arrow_alpha: bpy.props.FloatProperty(name="矢印の透明度", default=DEFAULT_ARROW_ALPHA, min=0.0, max=1.0, update=on_property_update)

# =========================================================
# Operators
# =========================================================
class SKELETON_CUBE_OT_CreateCube(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_cube"
    bl_label = "骨格立方体を作成"
    def execute(self, context):
        context.scene.skeleton_cube_props.cube_created = True
        update_skeleton_cube(context)
        return {'FINISHED'}

class SKELETON_CUBE_OT_DetachCube(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_cube"
    bl_label = "骨格立方体を独立させる"
    def execute(self, context):
        obj = bpy.data.objects.get(PREFIX_VAL)
        if obj:
            suffix = str(int(time.time() % 100000))
            obj.name = f"{PREFIX_VAL}_Fixed_{suffix}"
            if obj.data: obj.data.name = f"{PREFIX_VAL}_MESH_{suffix}"
            for slot in obj.material_slots:
                if slot.material:
                    new_mat = slot.material.copy()
                    new_mat.name = f"{slot.material.name}_{suffix}"
                    slot.material = new_mat
            context.scene.skeleton_cube_props.cube_created = False
        return {'FINISHED'}

class SKELETON_CUBE_OT_CreateSingle(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_single"
    bl_label = "単独球体を作成"
    def execute(self, context):
        context.scene.skeleton_cube_props.single_created = True
        update_single_sphere(context)
        return {'FINISHED'}

class SKELETON_CUBE_OT_DetachSingle(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_single"
    bl_label = "単独球体を独立させる"
    def execute(self, context):
        obj = bpy.data.objects.get(f"{PREFIX_VAL}_SINGLE_SPH")
        if obj:
            suffix = str(int(time.time() % 100000))
            obj.name = f"{PREFIX_VAL}_SINGLE_SPH_Fixed_{suffix}"
            if obj.data: obj.data.name = f"{obj.name}_MESH"
            for slot in obj.material_slots:
                if slot.material:
                    new_mat = slot.material.copy()
                    new_mat.name = f"{slot.material.name}_{suffix}"
                    slot.material = new_mat
            obj.matrix_world = obj.matrix_world.copy()
            obj.parent = None
            context.scene.skeleton_cube_props.single_created = False
        return {'FINISHED'}

class SKELETON_CUBE_OT_CreateArrow(bpy.types.Operator):
    bl_idname = f"{PREFIX}.create_arrow"
    bl_label = "矢印を更新・作成"
    def execute(self, context):
        context.scene.skeleton_cube_props.arrow_created = True
        update_arrow(context)
        return {'FINISHED'}

class SKELETON_CUBE_OT_DetachArrow(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach_arrow"
    bl_label = "矢印を独立させる"
    def execute(self, context):
        obj = bpy.data.objects.get(f"{PREFIX_VAL}_ARROW")
        if obj:
            suffix = str(int(time.time() % 100000))
            obj.name = f"{PREFIX_VAL}_ARROW_Fixed_{suffix}"
            if obj.data: obj.data.name = f"{obj.name}_MESH"
            for slot in obj.material_slots:
                if slot.material:
                    new_mat = slot.material.copy()
                    new_mat.name = f"{slot.material.name}_{suffix}"
                    slot.material = new_mat
            obj.matrix_world = obj.matrix_world.copy()
            obj.parent = None
            context.scene.skeleton_cube_props.arrow_created = False
        return {'FINISHED'}

class SKELETON_CUBE_OT_JoinSelected(bpy.types.Operator):
    bl_idname = f"{PREFIX}.join_selected"
    bl_label = "選択したObjectを一体化"
    bl_description = "2つ以上のオブジェクトを選択して1つにまとめます"
    bl_options = {'REGISTER', 'UNDO'}
    @classmethod
    def poll(cls, context): return len(context.selected_objects) >= 2 and context.active_object is not None
    def execute(self, context):
        bpy.ops.object.join()
        return {'FINISHED'}

class SKELETON_CUBE_OT_RemoveAddon(bpy.types.Operator):
    bl_idname = f"{PREFIX}.remove_addon"
    bl_label = "アドオン機能を消去する"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister() or None, first_interval=0.01)
        return {'FINISHED'}

# =========================================================
# Panels
# =========================================================
class SKELETON_CUBE_PT_Main(bpy.types.Panel):
    bl_label = ADDON_TITLE
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw_header(self, context):
        props = context.scene.skeleton_cube_props
        self.layout.prop(props, "view_show_cube", text="", icon='HIDE_OFF' if props.view_show_cube else 'HIDE_ON')

    def draw(self, context):
        layout = self.layout
        props = context.scene.skeleton_cube_props
        layout.prop(props, "target_collection_name", text="保存コレクション")
        layout.separator()
        col = layout.column(align=True)
        col.prop(props, "loc", text="全体位置")
        col.prop(props, "rot", text="全体回転")
        col.separator()
        col.label(text="【骨格 (円柱)】")
        col.prop(props, "edge_length", text="立方体1辺")
        col.prop(props, "cylinder_radius", text="円柱半径")
        col.prop(props, "color", text="色")
        col.prop(props, "alpha", text="透明度")
        col.separator()
        col.label(text="【頂点の球体】")
        col.prop(props, "show_vert_spheres", text="頂点球体を生成")
        sub = col.column(align=True)
        sub.enabled = props.show_vert_spheres
        sub.prop(props, "vert_sphere_radius", text="半径")
        sub.prop(props, "vert_sphere_color", text="色")
        sub.prop(props, "vert_sphere_alpha", text="透明度")
        col.separator()
        layout.operator(SKELETON_CUBE_OT_CreateCube.bl_idname, icon='CUBE')
        layout.operator(SKELETON_CUBE_OT_DetachCube.bl_idname, icon='UNLINKED')

class SKELETON_CUBE_PT_SingleSphere(bpy.types.Panel):
    bl_label = "単独球体の設定"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw_header(self, context):
        props = context.scene.skeleton_cube_props
        self.layout.prop(props, "view_show_single", text="", icon='HIDE_OFF' if props.view_show_single else 'HIDE_ON')

    def draw(self, context):
        layout = self.layout
        props = context.scene.skeleton_cube_props
        layout.prop(props, "show_single_sphere")
        sub = layout.column(align=True)
        sub.enabled = props.show_single_sphere
        sub.prop(props, "single_sphere_loc", text="相対位置")
        sub.prop(props, "single_sphere_radius", text="半径")
        sub.prop(props, "single_sphere_color", text="色")
        sub.prop(props, "single_sphere_alpha", text="透明度")
        layout.separator()
        layout.operator(SKELETON_CUBE_OT_CreateSingle.bl_idname, icon='SPHERE')
        layout.operator(SKELETON_CUBE_OT_DetachSingle.bl_idname, icon='UNLINKED')

class SKELETON_CUBE_PT_Arrow(bpy.types.Panel):
    bl_label = "矢印の設定 (UV操作)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw_header(self, context):
        props = context.scene.skeleton_cube_props
        self.layout.prop(props, "view_show_arrow", text="", icon='HIDE_OFF' if props.view_show_arrow else 'HIDE_ON')

    def draw(self, context):
        layout = self.layout
        props = context.scene.skeleton_cube_props
        layout.prop(props, "show_arrow")
        sub = layout.column(align=True)
        sub.enabled = props.show_arrow
        sub.prop(props, "arrow_u", text="U座標 (経度)")
        sub.prop(props, "arrow_v", text="V座標 (緯度)")
        sub.separator()
        sub.prop(props, "arrow_auto_length")
        len_col = sub.column(align=True)
        len_col.enabled = not props.arrow_auto_length
        len_col.prop(props, "arrow_length")
        sub.separator()
        sub.prop(props, "arrow_radius", text="矢印の太さ")
        sub.prop(props, "arrow_cone_len", text="先端の長さ")
        sub.prop(props, "arrow_color", text="色")
        sub.prop(props, "arrow_alpha", text="透明度")
        layout.separator()
        layout.operator(SKELETON_CUBE_OT_CreateArrow.bl_idname, icon='PLAY')
        layout.operator(SKELETON_CUBE_OT_DetachArrow.bl_idname, icon='UNLINKED')

class SKELETON_CUBE_PT_Join(bpy.types.Panel):
    bl_label = "オブジェクトの一体化"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        layout.label(text="※2つ以上のObjectを選択して実行", icon='INFO')
        layout.operator(SKELETON_CUBE_OT_JoinSelected.bl_idname, icon='LINKED')

class SKELETON_CUBE_PT_System(bpy.types.Panel):
    bl_label = "システム設定"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        layout.label(text="アドオンの完全消去を行います", icon='ERROR')
        layout.operator(SKELETON_CUBE_OT_RemoveAddon.bl_idname, icon='CANCEL')

# =========================================================
# Register / Unregister / Viewport Setup
# =========================================================
classes = (
    SkeletonCubeProperties,
    SKELETON_CUBE_OT_CreateCube, SKELETON_CUBE_OT_DetachCube,
    SKELETON_CUBE_OT_CreateSingle, SKELETON_CUBE_OT_DetachSingle,
    SKELETON_CUBE_OT_CreateArrow, SKELETON_CUBE_OT_DetachArrow,
    SKELETON_CUBE_OT_JoinSelected, SKELETON_CUBE_OT_RemoveAddon,
    SKELETON_CUBE_PT_Main, SKELETON_CUBE_PT_SingleSphere, 
    SKELETON_CUBE_PT_Arrow, SKELETON_CUBE_PT_Join, SKELETON_CUBE_PT_System,
)

def setup_viewport_on_load():
    context = bpy.context
    if context and context.window_manager:
        for window in context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            space.show_region_ui = True
                            space.shading.type = 'MATERIAL'
    return None

def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.skeleton_cube_props = bpy.props.PointerProperty(type=SkeletonCubeProperties)
    # 起動時のUIパネル展開&マテリアルプレビュー化
    bpy.app.timers.register(setup_viewport_on_load, first_interval=0.1)

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

if __name__ == "__main__":
    register()