# Code Created : 2024_0525_1400
bl_info = {
"name": "Skeleton Cube Generator",
"author": "Your Name",
"version": (1, 1, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "Generates a skeletal cube from integrated cylinders and spheres.",
"category": "3D View",
}
import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector, Matrix
# =========================================================
# 共通定義
# =========================================================
ADDON_TITLE = "Skeleton Cube"
PREFIX_VAL = "SKELETON_CUBE"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Skeleton Cube"
# =========================================================
# ユーザー設定パラメータ (初期値)
# =========================================================
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_SHOW_SPHERES = True
DEFAULT_SPHERE_RADIUS = 0.08
DEFAULT_SPH_COLOR_R = 1.0
DEFAULT_SPH_COLOR_G = 0.3
DEFAULT_SPH_COLOR_B = 0.3
DEFAULT_SPH_ALPHA = 1.0
# =========================================================
# リンク定義
# =========================================================
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>"
# =========================================================
# マテリアル設定補助関数
# =========================================================
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)
# 透過設定 (Eevee / Viewport互換)
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"
# 1. メッシュの取得・作成
mesh = bpy.data.meshes.get(mesh_name)
if not mesh:
mesh = bpy.data.meshes.new(mesh_name)
else:
mesh.clear_geometry()
# 2. オブジェクトの取得・作成
obj = bpy.data.objects.get(obj_name)
if not obj:
obj = bpy.data.objects.new(obj_name, mesh)
context.collection.objects.link(obj)
# 3. BMeshで 1 Mesh に 円柱 と 球体 を構築
bm = bmesh.new()
L = props.edge_length
h = L / 2.0
r_cyl = props.cylinder_radius
# 立方体の8頂点 (ローカル座標)
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))
]
v1, v2, v3, v4, v5, v6, v7, v8 = verts_pos
# 12本の辺の接続ペア
edges = [
(v1, v2), (v2, v3), (v3, v4), (v4, v1), # 下側
(v5, v6), (v6, v7), (v7, v8), (v8, v5), # 上側
(v1, v5), (v2, v6), (v3, v7), (v4, v8) # 縦方向
]
# --- 円柱の生成 (マテリアルインデックス 0) ---
for start, end in edges:
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
)
# --- 球体の生成 (マテリアルインデックス 1) ---
if props.show_spheres:
r_sph = props.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)
)
# 生成された球体のFaceにマテリアルインデックス1を割り当て
for i in range(start_face_count, len(bm.faces)):
bm.faces[i].material_index = 1
# BMeshのデータをMeshに書き込み
bm.to_mesh(mesh)
bm.free()
mesh.update()
# 4. オブジェクトのTransformを更新
obj.location = props.loc
obj.rotation_euler = props.rot
# 5. マテリアルの設定と割り当て
mat_cyl = setup_material(f"{PREFIX_VAL}_CYL_MAT", props.color, props.alpha)
mat_sph = setup_material(f"{PREFIX_VAL}_SPH_MAT", props.sphere_color, props.sphere_alpha)
obj.data.materials.clear()
obj.data.materials.append(mat_cyl) # Index 0: 円柱
obj.data.materials.append(mat_sph) # Index 1: 球体
# 6. ビューポートの強制更新
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
# =========================================================
# プロパティ・コールバック
# =========================================================
def on_property_update(self, context):
if self.is_created:
update_skeleton_cube(context)
class SkeletonCubeProperties(bpy.types.PropertyGroup):
is_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_spheres: bpy.props.BoolProperty(
name="頂点に球体を表示", default=DEFAULT_SHOW_SPHERES,
update=on_property_update
)
sphere_radius: bpy.props.FloatProperty(
name="球体半径", default=DEFAULT_SPHERE_RADIUS, min=0.001,
update=on_property_update
)
sphere_color: bpy.props.FloatVectorProperty(
name="球体色", subtype='COLOR', size=3, min=0.0, max=1.0,
default=(DEFAULT_SPH_COLOR_R, DEFAULT_SPH_COLOR_G, DEFAULT_SPH_COLOR_B),
update=on_property_update
)
sphere_alpha: bpy.props.FloatProperty(
name="球体透明度", default=DEFAULT_SPH_ALPHA, min=0.0, max=1.0,
update=on_property_update
)
# =========================================================
# Operators
# =========================================================
class SKELETON_CUBE_OT_Create(bpy.types.Operator):
bl_idname = f"{PREFIX}.create"
bl_label = "骨格立方体を作成"
bl_description = "現在の設定で骨格立方体を生成・更新します"
def execute(self, context):
props = context.scene.skeleton_cube_props
props.is_created = True
update_skeleton_cube(context)
return {'FINISHED'}
class SKELETON_CUBE_OT_OpenLink(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_link"
bl_label = "Open Link"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url:
webbrowser.open(self.url)
return {'FINISHED'}
def unregister_delayed():
try:
unregister()
except Exception as e:
print(f"Skeleton Cube Unregister Error: {e}")
return None # Timer解除
class SKELETON_CUBE_OT_RemoveAddon(bpy.types.Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "アドオンから切り離す"
bl_description = "現在の状態を確定し、アドオン機能を解除します。Objectは削除されません。"
def execute(self, context):
bpy.app.timers.register(unregister_delayed, 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
col = layout.column(align=True)
col.label(text="中心位置")
col.prop(props, "loc", index=0, text="X")
col.prop(props, "loc", index=1, text="Y")
col.prop(props, "loc", index=2, text="Z")
col.separator()
col.label(text="回転")
col.prop(props, "rot", index=0, text="X")
col.prop(props, "rot", index=1, text="Y")
col.prop(props, "rot", index=2, text="Z")
col.separator()
col.prop(props, "edge_length", text="立方体1辺")
col.prop(props, "cylinder_radius", text="円柱半径")
col.separator()
col.prop(props, "color", text="円柱の色")
col.prop(props, "alpha", text="円柱の透明度")
col.separator()
layout.operator(SKELETON_CUBE_OT_Create.bl_idname, text="骨格立方体を作成", icon='CUBE')
class SKELETON_CUBE_PT_Sphere(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
col = layout.column(align=True)
col.prop(props, "show_spheres", text="球体を生成する")
# チェックがオフならグレーアウト
sub = col.column(align=True)
sub.enabled = props.show_spheres
sub.prop(props, "sphere_radius", text="球体の半径")
sub.prop(props, "sphere_color", text="球体の色")
sub.prop(props, "sphere_alpha", text="球体の透明度")
class SKELETON_CUBE_PT_Links(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.operator(SKELETON_CUBE_OT_OpenLink.bl_idname, text="カスタムリンク (未設定)").url = ""
layout.operator(SKELETON_CUBE_OT_OpenLink.bl_idname, text=LINK_NAME_2).url = LINK_URL_2
layout.operator(SKELETON_CUBE_OT_OpenLink.bl_idname, text=LINK_NAME_3).url = LINK_URL_3
layout.operator(SKELETON_CUBE_OT_OpenLink.bl_idname, text=LINK_NAME_4).url = LINK_URL_4
class SKELETON_CUBE_PT_Remove(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="このアドオンを消去します")
layout.operator(SKELETON_CUBE_OT_RemoveAddon.bl_idname, text="アドオンから切り離す", icon='CANCEL')
# =========================================================
# Register / Unregister / 起動時設定
# =========================================================
classes = (
SkeletonCubeProperties,
SKELETON_CUBE_OT_Create,
SKELETON_CUBE_OT_OpenLink,
SKELETON_CUBE_OT_RemoveAddon,
SKELETON_CUBE_PT_Main,
SKELETON_CUBE_PT_Sphere,
SKELETON_CUBE_PT_Links,
SKELETON_CUBE_PT_Remove,
)
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()