骨格 立方体作成リスト

rapture_20260818232716.png

# Code Created : 2024_0526_1200

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

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

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

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

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

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

DEFAULT_SINGLE_SPHERE_SHOW = True
DEFAULT_SINGLE_SPHERE_LOC_X = 0.0
DEFAULT_SINGLE_SPHERE_LOC_Y = 0.0
DEFAULT_SINGLE_SPHERE_LOC_Z = 0.0
DEFAULT_SINGLE_SPHERE_RADIUS = 0.5
DEFAULT_SINGLE_SPH_COLOR_R = 0.2
DEFAULT_SINGLE_SPH_COLOR_G = 1.0
DEFAULT_SINGLE_SPH_COLOR_B = 0.2
DEFAULT_SINGLE_SPH_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)       # Index 0
    obj.data.materials.append(mat_vert_sph)  # Index 1

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 on_property_update(self, context):
    if self.cube_created:
        update_skeleton_cube(context)
    if self.single_created:
        update_single_sphere(context)
        
    for area in context.screen.areas:
        if area.type == 'VIEW_3D':
            area.tag_redraw()

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)
    
    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)

# =========================================================
# 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
            self.report({'INFO'}, f"立方体を独立させました: {obj.name}")
        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
            self.report({'INFO'}, f"単独球体を独立させました: {obj.name}")
        return {'FINISHED'}

class SKELETON_CUBE_OT_JoinSelected(bpy.types.Operator):
    bl_idname = f"{PREFIX}.join_selected"
    bl_label = "選択したObjectを一体化"
    bl_description = "2つ以上のオブジェクトを選択して1つにまとめます (Ctrl+Jと同じ機能)"
    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()
        self.report({'INFO'}, "選択されたオブジェクトを一体化しました")
        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(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(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_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
# =========================================================
classes = (
    SkeletonCubeProperties,
    SKELETON_CUBE_OT_CreateCube, 
    SKELETON_CUBE_OT_DetachCube,
    SKELETON_CUBE_OT_CreateSingle, 
    SKELETON_CUBE_OT_DetachSingle,
    SKELETON_CUBE_OT_JoinSelected,
    SKELETON_CUBE_OT_RemoveAddon,
    SKELETON_CUBE_PT_Main, 
    SKELETON_CUBE_PT_SingleSphere, 
    SKELETON_CUBE_PT_Join, 
    SKELETON_CUBE_PT_System,
)

def register():
    for cls in classes: 
        bpy.utils.register_class(cls)
    bpy.types.Scene.skeleton_cube_props = bpy.props.PointerProperty(type=SkeletonCubeProperties)

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()