骨格 立方体作成リスト

円柱の組み合わせ 一体化で 骨格立方体を

初期値000中心んに 作る

xyz 平行移動

xyz 回転

円柱 太さ 色 透明度

円柱1辺の長さ

骨格立方体になる 円柱12本の一体化

リアルタイムで 長さとかを 変更

作成ボタンと 

アドオンからの切り離し ボタン

アドオン切り離し後に Objectは 変更を受けない 形も色も

rapture_20260818223645.png

rapture_20260818223743.png

# =========================================================
# Blender 5+ Skeleton Cube Generator
# 12本の円柱を一体化した骨格立方体
# =========================================================

bl_info = {
    "name": "Skeleton Cube Generator",
    "author": "AI Assistant",
    "version": (1, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > Skeleton Cube",
    "description": "Generates a skeletal cube from 12 integrated cylinders.",
    "category": "3D View",
}

# =========================================================
# Code Created
# =========================================================
# Code Created : 2026_0818_2100

# =========================================================
# 1. import
# =========================================================

import bpy
import bmesh
import math
import webbrowser

from mathutils import Vector

# =========================================================
# 2. ユーザー設定パラメータ
# =========================================================

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_CYLINDER_SEGMENTS = 32

# =========================================================
# 3. 共通定義
# =========================================================

ADDON_TITLE = "Skeleton Cube"
PREFIX_VAL = "SKELETON_CUBE"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Skeleton Cube"

OBJECT_NAME = f"{PREFIX_VAL}_OBJECT"
MESH_NAME = f"{PREFIX_VAL}_MESH"
MATERIAL_NAME = f"{PREFIX_VAL}_MATERIAL"

# =========================================================
# 4. リンク定義
# =========================================================

# 1番はユーザー指定用として空ける

LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"

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

LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

# =========================================================
# 5. グローバル状態
# =========================================================

_IS_UPDATING = False
_IS_DETACHING = False

# =========================================================
# 6. Utility
# =========================================================

def get_skeleton_object():
    """このアドオンが生成したSkeleton Cubeを取得"""

    obj = bpy.data.objects.get(OBJECT_NAME)

    if obj and obj.type == 'MESH':
        return obj

    return None

# =========================================================
# 7. Material
# =========================================================

def get_or_create_material():
    mat = bpy.data.materials.get(MATERIAL_NAME)

    if mat is None:
        mat = bpy.data.materials.new(MATERIAL_NAME)

    mat.use_nodes = True

    nodes = mat.node_tree.nodes

    principled = nodes.get("Principled BSDF")

    if principled is None:
        principled = nodes.new("ShaderNodeBsdfPrincipled")

    return mat

def update_material(props):
    """
    色と透明度をMaterialへ反映
    """

    obj = get_skeleton_object()

    if obj is None:
        return

    mat = get_or_create_material()

    color = (
        props.color[0],
        props.color[1],
        props.color[2],
        props.opacity,
    )

    principled = mat.node_tree.nodes.get("Principled BSDF")

    if principled:

        # Blender 5.x のPrincipled BSDF
        if "Base Color" in principled.inputs:
            principled.inputs["Base Color"].default_value = color

        if "Alpha" in principled.inputs:
            principled.inputs["Alpha"].default_value = props.opacity

        # 透明度を実際に表示できるようにTransmission
        # ではなくAlpha設定を利用
        if "Roughness" in principled.inputs:
            principled.inputs["Roughness"].default_value = 0.35

    # Material Settings
    try:
        mat.surface_render_method = 'DITHERED'
    except Exception:
        pass

    try:
        mat.diffuse_color = color
    except Exception:
        pass

    # 既存マテリアルを整理
    obj.data.materials.clear()
    obj.data.materials.append(mat)

# =========================================================
# 8. 円柱メッシュ生成
# =========================================================

def add_cylinder_between(bm, start, end, radius, segments):
    """
    start → end の間に円柱を生成して
    bmeshへ直接追加する。

    Objectは生成しない。
    したがって最終的に12本すべてが
    1つのMeshになる。
    """

    start = Vector(start)
    end = Vector(end)

    direction = end - start
    length = direction.length

    if length <= 0.000001:
        return

    midpoint = (start + end) * 0.5

    # bmesh.ops.create_coneで円柱を生成
    result = bmesh.ops.create_cone(
        bm,
        cap_ends=True,
        cap_tris=False,
        segments=segments,
        radius1=radius,
        radius2=radius,
        depth=length,
    )

    verts = result["verts"]

    # 円柱は最初Z軸方向
    z_axis = Vector((0.0, 0.0, 1.0))
    direction_normalized = direction.normalized()

    rotation = z_axis.rotation_difference(direction_normalized)

    for v in verts:
        v.co = rotation @ v.co
        v.co += midpoint

# =========================================================
# 9. 12本の円柱による骨格立方体生成
# =========================================================

def build_skeleton_mesh(edge_length, radius):
    """
    12本の円柱を1つのbmeshへ直接生成する。
    """

    bm = bmesh.new()

    h = edge_length * 0.5

    # -----------------------------------------------------
    # 8頂点
    # -----------------------------------------------------

    v0 = Vector((-h, -h, -h))
    v1 = Vector(( h, -h, -h))
    v2 = Vector(( h,  h, -h))
    v3 = Vector((-h,  h, -h))

    v4 = Vector((-h, -h,  h))
    v5 = Vector(( h, -h,  h))
    v6 = Vector(( h,  h,  h))
    v7 = Vector((-h,  h,  h))

    edges = [

        # 下
        (v0, v1),
        (v1, v2),
        (v2, v3),
        (v3, v0),

        # 上
        (v4, v5),
        (v5, v6),
        (v6, v7),
        (v7, v4),

        # 縦
        (v0, v4),
        (v1, v5),
        (v2, v6),
        (v3, v7),
    ]

    # -----------------------------------------------------
    # 12本生成
    # -----------------------------------------------------

    for start, end in edges:
        add_cylinder_between(
            bm,
            start,
            end,
            radius,
            DEFAULT_CYLINDER_SEGMENTS,
        )

    # -----------------------------------------------------
    # Meshへ変換
    # -----------------------------------------------------

    mesh = bpy.data.meshes.new(MESH_NAME)

    bm.to_mesh(mesh)

    bm.free()

    mesh.update()

    return mesh

# =========================================================
# 10. Skeleton Cube生成
# =========================================================

def create_or_update_skeleton(context):
    global _IS_UPDATING

    if _IS_UPDATING:
        return

    if _IS_DETACHING:
        return

    props = context.scene.skeleton_cube_props

    _IS_UPDATING = True

    try:

        obj = get_skeleton_object()

        # -------------------------------------------------
        # Objectがない場合
        # -------------------------------------------------

        if obj is None:

            mesh = build_skeleton_mesh(
                props.edge_length,
                props.cylinder_radius,
            )

            obj = bpy.data.objects.new(
                OBJECT_NAME,
                mesh,
            )

            context.collection.objects.link(obj)

        # -------------------------------------------------
        # 既存Objectがある場合
        # Meshを作り直す
        # -------------------------------------------------

        else:

            new_mesh = build_skeleton_mesh(
                props.edge_length,
                props.cylinder_radius,
            )

            old_mesh = obj.data

            obj.data = new_mesh

            # 古いMeshを削除
            if old_mesh and old_mesh.users == 0:
                bpy.data.meshes.remove(old_mesh)

        # -------------------------------------------------
        # Transform
        # -------------------------------------------------

        obj.location = (
            props.center_x,
            props.center_y,
            props.center_z,
        )

        obj.rotation_mode = 'XYZ'

        obj.rotation_euler = (
            math.radians(props.rotation_x),
            math.radians(props.rotation_y),
            math.radians(props.rotation_z),
        )

        # -------------------------------------------------
        # Material
        # -------------------------------------------------

        update_material(props)

        # -------------------------------------------------
        # 表示
        # -------------------------------------------------

        obj.hide_viewport = False
        obj.hide_render = False

        obj.select_set(True)

        context.view_layer.objects.active = obj

    finally:

        _IS_UPDATING = False

# =========================================================
# 11. Property Update
# =========================================================

def property_update(self, context):
    """
    UIの数値変更時にリアルタイム更新
    """

    if _IS_DETACHING:
        return

    create_or_update_skeleton(context)

# =========================================================
# 12. PropertyGroup
# =========================================================

class SKELETON_CUBE_PG_Properties(bpy.types.PropertyGroup):

    # -----------------------------------------------------
    # Position
    # -----------------------------------------------------

    center_x: bpy.props.FloatProperty(
        name="位置 X",
        default=DEFAULT_CENTER_X,
        update=property_update,
    )

    center_y: bpy.props.FloatProperty(
        name="位置 Y",
        default=DEFAULT_CENTER_Y,
        update=property_update,
    )

    center_z: bpy.props.FloatProperty(
        name="位置 Z",
        default=DEFAULT_CENTER_Z,
        update=property_update,
    )

    # -----------------------------------------------------
    # Rotation
    # -----------------------------------------------------

    rotation_x: bpy.props.FloatProperty(
        name="回転 X",
        default=DEFAULT_ROTATION_X,
        update=property_update,
    )

    rotation_y: bpy.props.FloatProperty(
        name="回転 Y",
        default=DEFAULT_ROTATION_Y,
        update=property_update,
    )

    rotation_z: bpy.props.FloatProperty(
        name="回転 Z",
        default=DEFAULT_ROTATION_Z,
        update=property_update,
    )

    # -----------------------------------------------------
    # Cube
    # -----------------------------------------------------

    edge_length: bpy.props.FloatProperty(
        name="立方体1辺",
        default=DEFAULT_EDGE_LENGTH,
        min=0.001,
        soft_max=1000.0,
        update=property_update,
    )

    # -----------------------------------------------------
    # Cylinder
    # -----------------------------------------------------

    cylinder_radius: bpy.props.FloatProperty(
        name="円柱半径",
        default=DEFAULT_CYLINDER_RADIUS,
        min=0.0001,
        soft_max=10.0,
        update=property_update,
    )

    # -----------------------------------------------------
    # Color
    # -----------------------------------------------------

    color: bpy.props.FloatVectorProperty(
        name="色",
        subtype='COLOR',
        size=4,
        default=(
            DEFAULT_COLOR_R,
            DEFAULT_COLOR_G,
            DEFAULT_COLOR_B,
            DEFAULT_ALPHA,
        ),
        min=0.0,
        max=1.0,
        update=property_update,
    )

    # -----------------------------------------------------
    # Opacity
    # -----------------------------------------------------

    opacity: bpy.props.FloatProperty(
        name="透明度",
        default=DEFAULT_ALPHA,
        min=0.0,
        max=1.0,
        update=property_update,
    )

# =========================================================
# 13. Create Operator
# =========================================================

class SKELETON_CUBE_OT_Create(bpy.types.Operator):

    bl_idname = f"{PREFIX}.create"
    bl_label = "骨格立方体を作成"
    bl_description = "12本の円柱から骨格立方体を生成します"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):

        create_or_update_skeleton(context)

        self.report(
            {'INFO'},
            "骨格立方体を作成しました",
        )

        return {'FINISHED'}

# =========================================================
# 14. Link Operator
# =========================================================

class SKELETON_CUBE_OT_OpenUrl(bpy.types.Operator):

    bl_idname = f"{PREFIX}.open_url"
    bl_label = "リンクを開く"

    url: bpy.props.StringProperty()

    def execute(self, context):

        try:
            webbrowser.open(self.url)

        except Exception as e:

            self.report(
                {'ERROR'},
                f"URLを開けません: {e}",
            )

            return {'CANCELLED'}

        return {'FINISHED'}

# =========================================================
# 15. Addon Detach / Remove
# =========================================================

class SKELETON_CUBE_OT_RemoveAddon(bpy.types.Operator):

    bl_idname = f"{PREFIX}.remove_addon"

    bl_label = "アドオンから切り離す"

    bl_description = (
        "現在の骨格立方体を残したまま、"
        "アドオンを解除します"
    )

    def execute(self, context):

        global _IS_DETACHING

        # -------------------------------------------------
        # 切り離し開始
        # -------------------------------------------------

        _IS_DETACHING = True

        obj = get_skeleton_object()

        # -------------------------------------------------
        # 現在状態を確定
        # -------------------------------------------------

        if obj:

            obj.select_set(True)
            context.view_layer.objects.active = obj

            # 現在のGeometryを確定
            if obj.data:
                obj.data.update()

        # -------------------------------------------------
        # リアルタイムCallbackを停止
        # -------------------------------------------------

        # PropertyGroup解除前に状態を保持する
        scene = context.scene

        # -------------------------------------------------
        # 安全な遅延unregister
        # -------------------------------------------------

        try:
            bpy.app.timers.register(
                unregister,
                first_interval=0.01,
            )

        except Exception as e:

            _IS_DETACHING = False

            self.report(
                {'ERROR'},
                f"アドオン解除に失敗しました: {e}",
            )

            return {'CANCELLED'}

        self.report(
            {'INFO'},
            "アドオンから切り離しました。Objectは保持されています。",
        )

        return {'FINISHED'}

# =========================================================
# 16. Main Panel
# =========================================================

class SKELETON_CUBE_PT_Main(bpy.types.Panel):

    bl_label = "Skeleton Cube"
    bl_idname = f"{PREFIX_VAL}_PT_main"

    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    bl_order = 0

    def draw(self, context):

        layout = self.layout

        props = context.scene.skeleton_cube_props

        # -------------------------------------------------
        # Position
        # -------------------------------------------------

        box = layout.box()

        box.label(
            text="中心位置",
            icon='OBJECT_ORIGIN',
        )

        col = box.column(align=True)

        col.prop(
            props,
            "center_x",
        )

        col.prop(
            props,
            "center_y",
        )

        col.prop(
            props,
            "center_z",
        )

        # -------------------------------------------------
        # Rotation
        # -------------------------------------------------

        box = layout.box()

        box.label(
            text="回転",
            icon='ORIENTATION_GIMBAL',
        )

        col = box.column(align=True)

        col.prop(
            props,
            "rotation_x",
        )

        col.prop(
            props,
            "rotation_y",
        )

        col.prop(
            props,
            "rotation_z",
        )

        # -------------------------------------------------
        # Geometry
        # -------------------------------------------------

        box = layout.box()

        box.label(
            text="骨格立方体",
            icon='CUBE',
        )

        box.prop(
            props,
            "edge_length",
        )

        box.prop(
            props,
            "cylinder_radius",
        )

        # -------------------------------------------------
        # Material
        # -------------------------------------------------

        box = layout.box()

        box.label(
            text="マテリアル",
            icon='MATERIAL',
        )

        box.prop(
            props,
            "color",
            text="色",
        )

        box.prop(
            props,
            "opacity",
            text="透明度",
        )

        # -------------------------------------------------
        # Create
        # -------------------------------------------------

        layout.separator()

        row = layout.row()

        row.scale_y = 1.5

        row.operator(
            SKELETON_CUBE_OT_Create.bl_idname,
            icon='ADD',
        )

# =========================================================
# 17. Links Panel
# =========================================================

class SKELETON_CUBE_PT_LinksPanel(bpy.types.Panel):

    bl_label = "リンク"

    bl_idname = f"{PREFIX_VAL}_PT_links_panel"

    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    bl_category = ADDON_CATEGORY

    bl_order = 1

    def draw(self, context):

        layout = self.layout

        box = layout.box()

        if LINK_NAME_2:

            op2 = box.operator(
                SKELETON_CUBE_OT_OpenUrl.bl_idname,
                text=LINK_NAME_2,
                icon='URL',
            )

            op2.url = LINK_URL_2

        if LINK_NAME_3:

            op3 = box.operator(
                SKELETON_CUBE_OT_OpenUrl.bl_idname,
                text=LINK_NAME_3,
                icon='URL',
            )

            op3.url = LINK_URL_3

        if LINK_NAME_4:

            op4 = box.operator(
                SKELETON_CUBE_OT_OpenUrl.bl_idname,
                text=LINK_NAME_4,
                icon='URL',
            )

            op4.url = LINK_URL_4

# =========================================================
# 18. Remove / Detach Panel
# =========================================================

class SKELETON_CUBE_PT_RemovePanel(bpy.types.Panel):

    bl_label = "アドオンから切り離し"

    bl_idname = f"{PREFIX_VAL}_PT_remove_panel"

    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    bl_category = ADDON_CATEGORY

    bl_options = {'DEFAULT_CLOSED'}

    bl_order = 2

    def draw(self, context):

        layout = self.layout

        box = layout.box()

        box.label(
            text="Objectを残してアドオンを解除",
            icon='ERROR',
        )

        box.label(
            text="現在の状態で完全に固定されます。",
            icon='INFO',
        )

        box.separator()

        row = box.row()

        row.scale_y = 1.5

        row.operator(
            SKELETON_CUBE_OT_RemoveAddon.bl_idname,
            icon='UNLINKED',
        )

# =========================================================
# 19. Classes
# =========================================================

classes = (

    SKELETON_CUBE_PG_Properties,

    SKELETON_CUBE_OT_Create,
    SKELETON_CUBE_OT_OpenUrl,
    SKELETON_CUBE_OT_RemoveAddon,

    SKELETON_CUBE_PT_Main,
    SKELETON_CUBE_PT_LinksPanel,
    SKELETON_CUBE_PT_RemovePanel,
)

# =========================================================
# 20. Register
# =========================================================

def register():

    for cls in classes:

        try:
            bpy.utils.register_class(cls)

        except ValueError:

            # 二重登録対策
            try:
                bpy.utils.unregister_class(cls)
            except Exception:
                pass

            bpy.utils.register_class(cls)

    bpy.types.Scene.skeleton_cube_props = bpy.props.PointerProperty(
        type=SKELETON_CUBE_PG_Properties
    )

# =========================================================
# 21. Unregister
# =========================================================

def unregister():

    global _IS_DETACHING

    _IS_DETACHING = True

    # -----------------------------------------------------
    # Property解除
    # -----------------------------------------------------

    if hasattr(
        bpy.types.Scene,
        "skeleton_cube_props",
    ):

        del bpy.types.Scene.skeleton_cube_props

    # -----------------------------------------------------
    # Class解除
    # -----------------------------------------------------

    for cls in reversed(classes):

        try:
            bpy.utils.unregister_class(cls)

        except RuntimeError:
            pass

        except Exception:
            pass

    # -----------------------------------------------------
    # 重要
    #
    # 生成Objectは削除しない。
    #
    # bpy.data.objects.remove()
    # bpy.data.meshes.remove()
    #
    # はここでは実行しない。
    # -----------------------------------------------------

# =========================================================
# 22. Run
# =========================================================

if __name__ == "__main__":

    register()