完成品2026

rapture_20260714064028.png

bl_info = {
    "name": "zionad_ [ 骨格立方体 ]",
    "author": "zionad_",
    "version": (2, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > 骨格立方体",
    "description": "表面・骨格別の色設定、表面有無の選択が可能な骨格生成ツール",
    "category": "骨格立方体",
}

# 2026年07月14日 06時35分00秒

import bpy
import math
import webbrowser
import random
from bpy.props import FloatProperty, FloatVectorProperty, StringProperty, IntProperty, EnumProperty, BoolProperty
from bpy.types import Operator, Panel

# ==========================================
# 【初期設定・手動変更エリア】
# ==========================================

LINK_NAME_1 = "基本アドオン設定 20260711b"
LINK_URL_1 = "<https://app.notion.com/p/20260711-39af5dacaf438027811dcd877db7c630>"

LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"

LINK_NAME_4 = "blender コンテナ 20260713"
LINK_URL_4 = "<https://note.com/zionadmillion/n/n9a47d88fe7b4>"

LINK_NAME_5 = "骨格立方体"
LINK_URL_5 = "<https://app.notion.com/p/39cf5dacaf4380b7bda4cb5977f04606>"

OBJ_NAME_BASE = "Skeleton_Dice"
COL_NAME = "Dice_Collection"
SUB_COL_NAME = "Skeleton_Cubes"

# 初期数値
INIT_SIZE = 60.0
INIT_THICKNESS = 1.0
INIT_RES = 32           # 分割数初期値 32
INIT_POS = (0.0, 0.0, 0.0)
INIT_ROT = (0.0, 0.0, 0.0)

# 初期色 (骨格: フレーム用)
INIT_CLR_FRAME = (0.1, 0.6, 1.0, 1.0) # 青系
# 初期色 (表面: 面用)
INIT_CLR_FACE = (1.0, 1.0, 1.0, 0.3)  # 白の透明

# ==========================================
# 【形状生成コア・ロジック】
# ==========================================

def create_advanced_geometry(context):
    """骨格(パイプ)と表面(面)を組み合わせたメッシュを構築"""
    scene = context.scene
    import bmesh
    
    # 1. 元となる形状のデータを作成
    bm_base = bmesh.new()
    if scene.dice_shape == 'CUBE':
        bmesh.ops.create_cube(bm_base, size=scene.dice_size)
    else: # SQUARE
        bmesh.ops.create_grid(bm_base, x_segments=1, y_segments=1, size=scene.dice_size/2)
    
    # 2. パイプ骨格の作成
    curve_data = bpy.data.curves.new("temp_curve", type='CURVE')
    curve_data.dimensions = '3D'
    curve_data.fill_mode = 'FULL'
    curve_data.bevel_depth = scene.dice_thick / 2
    curve_data.bevel_resolution = scene.dice_res
    
    for edge in bm_base.edges:
        v1, v2 = edge.verts[0].co, edge.verts[1].co
        polyline = curve_data.splines.new('POLY')
        polyline.points.add(1)
        polyline.points[0].co = (*v1, 1)
        polyline.points[1].co = (*v2, 1)
    
    # 評価済みメッシュとして骨格を取得
    temp_frame_obj = bpy.data.objects.new("temp_frame", curve_data)
    context.collection.objects.link(temp_frame_obj)
    depsgraph = context.evaluated_depsgraph_get()
    mesh_frame = bpy.data.meshes.new_from_object(temp_frame_obj.evaluated_get(depsgraph))
    
    # 後片付け
    bpy.data.objects.remove(temp_frame_obj)
    bpy.data.curves.remove(curve_data)
    
    # 3. 結合用のBMesh作成
    bm_final = bmesh.new()
    # 骨格をBMeshに追加 (Material Index 0)
    bm_final.from_mesh(mesh_frame)
    for face in bm_final.faces:
        face.material_index = 0
    bpy.data.meshes.remove(mesh_frame)
    
    # 表面を追加する場合 (Material Index 1)
    if scene.dice_show_faces:
        # 面だけを抽出して追加
        bm_face = bmesh.new()
        if scene.dice_shape == 'CUBE':
            bmesh.ops.create_cube(bm_face, size=scene.dice_size)
        else:
            bmesh.ops.create_grid(bm_face, x_segments=1, y_segments=1, size=scene.dice_size/2)
        
        # 既存のBMeshに結合
        for face in bm_face.faces:
            face.material_index = 1
        bm_face.to_mesh(bpy.data.meshes.new("temp_face")) # 変換経由
        bm_final.from_mesh(bpy.data.meshes.get("temp_face"))
        bpy.data.meshes.remove(bpy.data.meshes.get("temp_face"))
        bm_face.free()

    # 4. 最終メッシュ出力
    mesh_out = bpy.data.meshes.new(OBJ_NAME_BASE)
    bm_final.to_mesh(mesh_out)
    bm_final.free()
    bm_base.free()
    
    return mesh_out

def update_skeleton_live(self, context):
    scene = context.scene
    for obj in scene.objects:
        if obj.name.startswith(OBJ_NAME_BASE) and obj.type == 'MESH':
            # 変形
            obj.location = scene.dice_pos
            obj.rotation_euler = [math.radians(a) for a in scene.dice_rot]
            
            # メッシュ更新
            new_mesh = create_advanced_geometry(context)
            old_mesh = obj.data
            obj.data = new_mesh
            if old_mesh: bpy.data.meshes.remove(old_mesh)
            
            # マテリアル設定 (2スロット)
            while len(obj.data.materials) < 2:
                obj.data.materials.append(None)
            
            # スロット0: 骨格用
            if not obj.material_slots[0].material:
                obj.material_slots[0].material = bpy.data.materials.new(name="Mat_Frame")
            mat_frame = obj.material_slots[0].material
            mat_frame.use_nodes = True
            bsdf_frame = mat_frame.node_tree.nodes.get("Principled BSDF")
            if bsdf_frame:
                bsdf_frame.inputs['Base Color'].default_value = scene.dice_color_frame
                if 'Alpha' in bsdf_frame.inputs: bsdf_frame.inputs['Alpha'].default_value = scene.dice_color_frame[3]
            mat_frame.blend_method = 'BLEND'

            # スロット1: 表面用
            if not obj.material_slots[1].material:
                obj.material_slots[1].material = bpy.data.materials.new(name="Mat_Face")
            mat_face = obj.material_slots[1].material
            mat_face.use_nodes = True
            bsdf_face = mat_face.node_tree.nodes.get("Principled BSDF")
            if bsdf_face:
                bsdf_face.inputs['Base Color'].default_value = scene.dice_color_face
                if 'Alpha' in bsdf_face.inputs: bsdf_face.inputs['Alpha'].default_value = scene.dice_color_face[3]
            mat_face.blend_method = 'BLEND'

# ==========================================
# 【オペレーター】
# ==========================================

class DICE_TOOLS_5_OT_Create(Operator):
    bl_idname = "dice_tools_5.create_skeleton"
    bl_label = "骨格を新規生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        mesh_data = create_advanced_geometry(context)
        obj = bpy.data.objects.new(f"{OBJ_NAME_BASE}_{random.randint(100,999)}", mesh_data)
        
        main_col = bpy.data.collections.get(COL_NAME) or bpy.data.collections.new(COL_NAME)
        if main_col.name not in context.scene.collection.children:
            context.scene.collection.children.link(main_col)
        sub_col = bpy.data.collections.get(SUB_COL_NAME) or bpy.data.collections.new(SUB_COL_NAME)
        if sub_col.name not in main_col.children:
            main_col.children.link(sub_col)
        sub_col.objects.link(obj)
        
        context.view_layer.objects.active = obj
        update_skeleton_live(self, context)
        return {'FINISHED'}

class DICE_TOOLS_5_OT_Detach(Operator):
    bl_idname = "dice_tools_5.detach_object"
    bl_label = "アドオンから切り離す"
    def execute(self, context):
        obj = context.active_object
        if obj and obj.name.startswith(OBJ_NAME_BASE):
            obj.name = "Fixed_Dice_" + str(random.randint(100,999))
            self.report({'INFO'}, "独立させました")
        return {'FINISHED'}

class DICE_TOOLS_5_OT_ViewCenter(Operator):
    bl_idname = "dice_tools_5.view_center"
    bl_label = "選択を中央へ"
    def execute(self, context):
        if context.active_object: bpy.ops.view3d.view_selected()
        return {'FINISHED'}

class DICE_TOOLS_5_OT_OpenLink(Operator):
    bl_idname = "dice_tools_5.open_link"
    bl_label = "URLを開く"
    url: StringProperty()
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

class DICE_TOOLS_5_OT_RemoveAddon(Operator):
    bl_idname = "dice_tools_5.remove_addon_ui"
    bl_label = "アドオンUIを削除"
    def execute(self, context):
        unregister()
        return {'FINISHED'}

# ==========================================
# 【UIパネル】
# ==========================================

class DICE_TOOLS_5_PT_ViewPanel(Panel):
    bl_label = "ビュー情報"
    bl_idname = "DICE_TOOLS_5_PT_ViewPanel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "骨格立方体"

    def draw(self, context):
        layout = self.layout
        rv3d = context.region_data
        layout.operator("dice_tools_5.view_center", icon='CENTER_ONLY')
        layout.separator()
        layout.label(text="モード: " + ("透視投影" if rv3d.view_perspective == 'PERSP' else "平行投影"))
        box = layout.box()
        loc = rv3d.view_location
        box.label(text=f"中心: X:{loc.x:.1f} Y:{loc.y:.1f} Z:{loc.z:.1f}")
        layout.label(text=f"画面: {context.region.width} x {context.region.height}")
        if rv3d.view_perspective == 'PERSP':
            layout.label(text=f"距離: {rv3d.view_distance:.1f}")
        layout.prop(context.scene, "dice_info_str", text="", icon='COPY_ID')

class DICE_TOOLS_5_PT_CreationPanel(Panel):
    bl_label = "骨格 調整"
    bl_idname = "DICE_TOOLS_5_PT_CreationPanel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "骨格立方体"

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        obj = context.active_object
        
        layout.operator("dice_tools_5.create_skeleton", icon='MESH_CUBE')
        layout.separator()
        
        col = layout.column(align=True)
        col.prop(scene, "dice_shape", text="基本形状")
        col.prop(scene, "dice_pos", text="位置")
        col.prop(scene, "dice_rot", text="回転")
        col.prop(scene, "dice_size", text="サイズ")
        col.prop(scene, "dice_thick", text="骨格の太さ")
        col.prop(scene, "dice_res", text="分割数")
        
        layout.separator()
        layout.prop(scene, "dice_show_faces", text="表面を表示する", icon='FACESEL')
        
        row = layout.row()
        row.prop(scene, "dice_color_frame", text="骨格の色")
        if scene.dice_show_faces:
            row.prop(scene, "dice_color_face", text="表面の色")
        
        layout.separator()
        if obj and obj.name.startswith(OBJ_NAME_BASE):
            layout.operator("dice_tools_5.detach_object", icon='UNLINKED')
        else:
            layout.label(text="※生成した骨格を選択して調整", icon='INFO')

class DICE_TOOLS_5_PT_Footer(Panel):
    bl_label = "リンク・管理"
    bl_idname = "DICE_TOOLS_5_PT_Footer"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "骨格立方体"
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        links = [(LINK_NAME_1, LINK_URL_1), (LINK_NAME_2, LINK_URL_2), (LINK_NAME_3, LINK_URL_3), (LINK_NAME_4, LINK_URL_4), (LINK_NAME_5, LINK_URL_5)]
        for name, url in links:
            op = layout.operator("dice_tools_5.open_link", text=name, icon='WORLD')
            op.url = url
        layout.separator()
        layout.operator("dice_tools_5.remove_addon_ui", icon='TRASH')

# ==========================================
# 【登録・解除】
# ==========================================

classes = (
    DICE_TOOLS_5_OT_ViewCenter, DICE_TOOLS_5_OT_Create, DICE_TOOLS_5_OT_Detach,
    DICE_TOOLS_5_OT_OpenLink, DICE_TOOLS_5_OT_RemoveAddon,
    DICE_TOOLS_5_PT_ViewPanel, DICE_TOOLS_5_PT_CreationPanel, DICE_TOOLS_5_PT_Footer,
)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    
    s = bpy.types.Scene
    s.dice_shape = EnumProperty(items=[('CUBE', "立方体", ""), ('SQUARE', "正方形", "")], name="Shape", default='CUBE', update=update_skeleton_live)
    s.dice_size = FloatProperty(name="Size", default=INIT_SIZE, min=0.1, update=update_skeleton_live)
    s.dice_thick = FloatProperty(name="Thick", default=INIT_THICKNESS, min=0.01, update=update_skeleton_live)
    s.dice_res = IntProperty(name="Res", default=INIT_RES, min=3, max=100, update=update_skeleton_live)
    s.dice_pos = FloatVectorProperty(name="Pos", default=INIT_POS, update=update_skeleton_live)
    s.dice_rot = FloatVectorProperty(name="Rot", default=INIT_ROT, update=update_skeleton_live)
    
    s.dice_show_faces = BoolProperty(name="Show Faces", default=True, update=update_skeleton_live)
    s.dice_color_frame = FloatVectorProperty(name="Color Frame", subtype='COLOR', size=4, default=INIT_CLR_FRAME, min=0.0, max=1.0, update=update_skeleton_live)
    s.dice_color_face = FloatVectorProperty(name="Color Face", subtype='COLOR', size=4, default=INIT_CLR_FACE, min=0.0, max=1.0, update=update_skeleton_live)
    
    s.dice_info_str = StringProperty(name="Info")

def unregister():
    for cls in reversed(classes):
        if hasattr(bpy.utils, "unregister_class"):
            try: bpy.utils.unregister_class(cls)
            except: pass
    
    s = bpy.types.Scene
    del s.dice_shape, s.dice_size, s.dice_thick, s.dice_res, s.dice_pos, s.dice_rot
    del s.dice_show_faces, s.dice_color_frame, s.dice_color_face, s.dice_info_str

if __name__ == "__main__":
    register()