bl_info = {
"name": "zionad_ [ 穴あき球体ツール・プロ ]",
"author": "zionad_",
"version": (3, 1, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "円錐で球体をくり抜き、内側と外側の色を個別に設定。切り離し・設定コピー機能付き",
"category": "3D View",
}
import bpy
import bmesh
import math
import random
# ===================================
# 共通定義
# ===================================
ADDON_TITLE = "穴あき球体"
ADDON_CATEGORY = "穴あき球体"
PREFIX_VAL = "pierced_sphere_v3"
PREFIX = PREFIX_VAL.lower()
# ===================================
# 定数 (初期値)
# ===================================
INIT_OBJ_NAME = "Pierced_Sphere"
INIT_SPHERE_SIZE = 10.0
INIT_CONE_ANGLE = 45.0
INIT_CONE_LENGTH = 15.0
INIT_COLOR_OUTER = (0.1, 0.1, 0.1, 1.0) # 外側:黒
INIT_COLOR_INNER = (1.0, 0.2, 0.0, 1.0) # 内側:オレンジ
# ===================================
# コアロジック (更新系)
# ===================================
def update_geometry(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
obj = context.active_object
# アドオンが管理しているオブジェクトかチェック
if not obj or not obj.name.startswith(props.obj_name):
return
# 1. 球体メッシュの更新
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=props.sphere_size)
for face in bm.faces:
face.material_index = 0
new_mesh = bpy.data.meshes.new(props.obj_name)
bm.to_mesh(new_mesh)
bm.free()
old_mesh = obj.data
obj.data = new_mesh
if old_mesh: bpy.data.meshes.remove(old_mesh)
# 2. カッター(円錐)の更新
cutter_name = f"{obj.name}_Cutter"
cutter = bpy.data.objects.get(cutter_name)
if not cutter:
cutter_mesh = bpy.data.meshes.new(cutter_name)
cutter = bpy.data.objects.new(cutter_name, cutter_mesh)
context.collection.objects.link(cutter)
cutter.hide_viewport = True
cutter.hide_render = True
bm_cone = bmesh.new()
rad_angle = math.radians(props.cone_angle / 2)
radius = props.cone_length * math.tan(rad_angle)
bmesh.ops.create_cone(
bm_cone, cap_ends=True, segments=64,
radius1=radius, radius2=0, depth=props.cone_length
)
# 頂点を0,0,0にする移動
bmesh.ops.translate(bm_cone, vec=(0, 0, props.cone_length/2), verts=bm_cone.verts)
for face in bm_cone.faces:
face.material_index = 1
bm_cone.to_mesh(cutter.data)
bm_cone.free()
# 3. モディファイア設定
mod_name = "Hole_Boolean"
mod = obj.modifiers.get(mod_name) or obj.modifiers.new(mod_name, 'BOOLEAN')
mod.operation = 'DIFFERENCE'
mod.object = cutter
mod.solver = 'EXACT'
# 4. マテリアル更新
setup_materials(obj, props)
def setup_materials(obj, props):
while len(obj.data.materials) < 2:
obj.data.materials.append(None)
def get_or_create_mat(name_suffix, color):
mat_name = f"{PREFIX_VAL}_{name_suffix}"
mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(mat_name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs['Base Color'].default_value = color
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = color[3]
mat.blend_method = 'BLEND' if color[3] < 1.0 else 'OPAQUE'
return mat
obj.data.materials[0] = get_or_create_mat("Outer", props.color_outer)
obj.data.materials[1] = get_or_create_mat("Inner", props.color_inner)
# ===================================
# オペレーター
# ===================================
class OT_create_pierced_sphere(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_create_sphere"
bl_label = "穴あき球体を生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
mesh = bpy.data.meshes.new(props.obj_name)
obj = bpy.data.objects.new(f"{props.obj_name}_{random.randint(100,999)}", mesh)
context.collection.objects.link(obj)
context.view_layer.objects.active = obj
update_geometry(self, context)
return {'FINISHED'}
class OT_detach_object(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_detach"
bl_label = "アドオンから切り離す"
bl_description = "名前を変更し、形状を確定させて独立させます"
def execute(self, context):
obj = context.active_object
if obj:
# ブーリアンを適用してメッシュを確定させる
for mod in obj.modifiers:
if mod.type == 'BOOLEAN':
bpy.ops.object.modifier_apply(modifier=mod.name)
# カッターを削除
cutter_name = f"{obj.name}_Cutter"
cutter = bpy.data.objects.get(cutter_name)
if cutter:
bpy.data.objects.remove(cutter, do_unlink=True)
# 名前を変更して管理外にする
obj.name = f"Fixed_Sphere_{random.randint(100,999)}"
self.report({'INFO'}, "オブジェクトを独立させました")
return {'FINISHED'}
class OT_copy_settings(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_copy_settings"
bl_label = "設定値をコピー"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
text = (
f"INIT_OBJ_NAME = '{props.obj_name}'\n"
f"INIT_SPHERE_SIZE = {props.sphere_size:.4f}\n"
f"INIT_CONE_ANGLE = {props.cone_angle:.4f}\n"
f"INIT_CONE_LENGTH = {props.cone_length:.4f}\n"
f"INIT_COLOR_OUTER = ({props.color_outer[0]:.4f}, {props.color_outer[1]:.4f}, {props.color_outer[2]:.4f}, {props.color_outer[3]:.4f})\n"
f"INIT_COLOR_INNER = ({props.color_inner[0]:.4f}, {props.color_inner[1]:.4f}, {props.color_inner[2]:.4f}, {props.color_inner[3]:.4f})\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "初期値用コードをコピーしました")
return {'FINISHED'}
class OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ===================================
# UI / プロパティ
# ===================================
class Properties_Addon(bpy.types.PropertyGroup):
obj_name: bpy.props.StringProperty(name="Object名ベース", default=INIT_OBJ_NAME)
sphere_size: bpy.props.FloatProperty(name="球体サイズ", default=INIT_SPHERE_SIZE, min=0.1, update=update_geometry)
cone_angle: bpy.props.FloatProperty(name="穴の角度", default=INIT_CONE_ANGLE, min=1.0, max=179.0, update=update_geometry)
cone_length: bpy.props.FloatProperty(name="穴の長さ", default=INIT_CONE_LENGTH, min=0.1, update=update_geometry)
color_outer: bpy.props.FloatVectorProperty(name="外側の色", subtype='COLOR', size=4, default=INIT_COLOR_OUTER, min=0.0, max=1.0, update=update_geometry)
color_inner: bpy.props.FloatVectorProperty(name="内側の色", subtype='COLOR', size=4, default=INIT_COLOR_INNER, min=0.0, max=1.0, update=update_geometry)
class PT_main_panel(bpy.types.Panel):
bl_label = "穴あき球体ツール"
bl_idname = f"{PREFIX.upper()}_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX}_props")
obj = context.active_object
# 生成ボタン
layout.operator(f"object.{PREFIX}_create_sphere", icon='MESH_UVSPHERE')
layout.separator()
# パラメータ設定
box = layout.box()
box.label(text="形状・色設定", icon='PREFERENCES')
box.prop(props, "sphere_size")
box.prop(props, "cone_angle")
box.prop(props, "cone_length")
box.separator()
box.prop(props, "color_outer")
box.prop(props, "color_inner")
layout.separator()
# 切離しボタン(現在のアクティブオブジェクトが対象の場合のみ有効)
if obj and obj.name.startswith(props.obj_name):
layout.operator(f"object.{PREFIX}_detach", icon='UNLINKED')
else:
layout.label(text="※生成したオブジェクトを選択して調整", icon='INFO')
class PT_system_panel(bpy.types.Panel):
bl_label = "システム"
bl_idname = f"{PREFIX.upper()}_PT_system"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.operator(f"object.{PREFIX}_copy_settings", icon='COPYDOWN')
layout.separator()
layout.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')
# ===================================
# 登録・解除
# ===================================
classes = [
Properties_Addon,
OT_create_pierced_sphere,
OT_detach_object,
OT_copy_settings,
OT_remove_addon,
PT_main_panel,
PT_system_panel,
]
def register():
for c in classes:
bpy.utils.register_class(c)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=Properties_Addon))
def unregister():
for c in reversed(classes):
bpy.utils.unregister_class(c)
if hasattr(bpy.types.Scene, f"{PREFIX}_props"):
delattr(bpy.types.Scene, f"{PREFIX}_props")
if __name__ == "__main__":
register()
bl_info = {
"name": "zionad_ [ 穴あき球体ツール・プロ ]",
"author": "zionad_",
"version": (3, 1, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "円錐で球体をくり抜き、内側と外側の色を個別に設定。切り離し・設定コピー機能付き",
"category": "3D View",
}
import bpy
import bmesh
import math
import random
# ===================================
# 共通定義
# ===================================
ADDON_TITLE = "穴あき球体"
ADDON_CATEGORY = "穴あき球体"
PREFIX_VAL = "pierced_sphere_v3"
PREFIX = PREFIX_VAL.lower()
# ===================================
# 定数 (初期値)
# ===================================
INIT_OBJ_NAME = "Pierced_Sphere"
INIT_SPHERE_SIZE = 10.0
INIT_CONE_ANGLE = 45.0
INIT_CONE_LENGTH = 15.0
INIT_COLOR_OUTER = (0.1, 0.1, 0.1, 1.0) # 外側:黒
INIT_COLOR_INNER = (1.0, 0.2, 0.0, 1.0) # 内側:オレンジ
# ===================================
# コアロジック (更新系)
# ===================================
def update_geometry(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
obj = context.active_object
# アドオンが管理しているオブジェクトかチェック
if not obj or not obj.name.startswith(props.obj_name):
return
# 1. 球体メッシュの更新
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=props.sphere_size)
for face in bm.faces:
face.material_index = 0
new_mesh = bpy.data.meshes.new(props.obj_name)
bm.to_mesh(new_mesh)
bm.free()
old_mesh = obj.data
obj.data = new_mesh
if old_mesh: bpy.data.meshes.remove(old_mesh)
# 2. カッター(円錐)の更新
cutter_name = f"{obj.name}_Cutter"
cutter = bpy.data.objects.get(cutter_name)
if not cutter:
cutter_mesh = bpy.data.meshes.new(cutter_name)
cutter = bpy.data.objects.new(cutter_name, cutter_mesh)
context.collection.objects.link(cutter)
cutter.hide_viewport = True
cutter.hide_render = True
bm_cone = bmesh.new()
rad_angle = math.radians(props.cone_angle / 2)
radius = props.cone_length * math.tan(rad_angle)
bmesh.ops.create_cone(
bm_cone, cap_ends=True, segments=64,
radius1=radius, radius2=0, depth=props.cone_length
)
# 頂点を0,0,0にする移動
bmesh.ops.translate(bm_cone, vec=(0, 0, props.cone_length/2), verts=bm_cone.verts)
for face in bm_cone.faces:
face.material_index = 1
bm_cone.to_mesh(cutter.data)
bm_cone.free()
# 3. モディファイア設定
mod_name = "Hole_Boolean"
mod = obj.modifiers.get(mod_name) or obj.modifiers.new(mod_name, 'BOOLEAN')
mod.operation = 'DIFFERENCE'
mod.object = cutter
mod.solver = 'EXACT'
# 4. マテリアル更新
setup_materials(obj, props)
def setup_materials(obj, props):
while len(obj.data.materials) < 2:
obj.data.materials.append(None)
def get_or_create_mat(name_suffix, color):
mat_name = f"{PREFIX_VAL}_{name_suffix}"
mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(mat_name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs['Base Color'].default_value = color
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = color[3]
mat.blend_method = 'BLEND' if color[3] < 1.0 else 'OPAQUE'
return mat
obj.data.materials[0] = get_or_create_mat("Outer", props.color_outer)
obj.data.materials[1] = get_or_create_mat("Inner", props.color_inner)
# ===================================
# オペレーター
# ===================================
class OT_create_pierced_sphere(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_create_sphere"
bl_label = "穴あき球体を生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
mesh = bpy.data.meshes.new(props.obj_name)
obj = bpy.data.objects.new(f"{props.obj_name}_{random.randint(100,999)}", mesh)
context.collection.objects.link(obj)
context.view_layer.objects.active = obj
update_geometry(self, context)
return {'FINISHED'}
class OT_detach_object(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_detach"
bl_label = "アドオンから切り離す"
bl_description = "名前を変更し、形状を確定させて独立させます"
def execute(self, context):
obj = context.active_object
if obj:
# ブーリアンを適用してメッシュを確定させる
for mod in obj.modifiers:
if mod.type == 'BOOLEAN':
bpy.ops.object.modifier_apply(modifier=mod.name)
# カッターを削除
cutter_name = f"{obj.name}_Cutter"
cutter = bpy.data.objects.get(cutter_name)
if cutter:
bpy.data.objects.remove(cutter, do_unlink=True)
# 名前を変更して管理外にする
obj.name = f"Fixed_Sphere_{random.randint(100,999)}"
self.report({'INFO'}, "オブジェクトを独立させました")
return {'FINISHED'}
class OT_copy_settings(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_copy_settings"
bl_label = "設定値をコピー"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
text = (
f"INIT_OBJ_NAME = '{props.obj_name}'\n"
f"INIT_SPHERE_SIZE = {props.sphere_size:.4f}\n"
f"INIT_CONE_ANGLE = {props.cone_angle:.4f}\n"
f"INIT_CONE_LENGTH = {props.cone_length:.4f}\n"
f"INIT_COLOR_OUTER = ({props.color_outer[0]:.4f}, {props.color_outer[1]:.4f}, {props.color_outer[2]:.4f}, {props.color_outer[3]:.4f})\n"
f"INIT_COLOR_INNER = ({props.color_inner[0]:.4f}, {props.color_inner[1]:.4f}, {props.color_inner[2]:.4f}, {props.color_inner[3]:.4f})\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "初期値用コードをコピーしました")
return {'FINISHED'}
class OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ===================================
# UI / プロパティ
# ===================================
class Properties_Addon(bpy.types.PropertyGroup):
obj_name: bpy.props.StringProperty(name="Object名ベース", default=INIT_OBJ_NAME)
sphere_size: bpy.props.FloatProperty(name="球体サイズ", default=INIT_SPHERE_SIZE, min=0.1, update=update_geometry)
cone_angle: bpy.props.FloatProperty(name="穴の角度", default=INIT_CONE_ANGLE, min=1.0, max=179.0, update=update_geometry)
cone_length: bpy.props.FloatProperty(name="穴の長さ", default=INIT_CONE_LENGTH, min=0.1, update=update_geometry)
color_outer: bpy.props.FloatVectorProperty(name="外側の色", subtype='COLOR', size=4, default=INIT_COLOR_OUTER, min=0.0, max=1.0, update=update_geometry)
color_inner: bpy.props.FloatVectorProperty(name="内側の色", subtype='COLOR', size=4, default=INIT_COLOR_INNER, min=0.0, max=1.0, update=update_geometry)
class PT_main_panel(bpy.types.Panel):
bl_label = "穴あき球体ツール"
bl_idname = f"{PREFIX.upper()}_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX}_props")
obj = context.active_object
# 生成ボタン
layout.operator(f"object.{PREFIX}_create_sphere", icon='MESH_UVSPHERE')
layout.separator()
# パラメータ設定
box = layout.box()
box.label(text="形状・色設定", icon='PREFERENCES')
box.prop(props, "sphere_size")
box.prop(props, "cone_angle")
box.prop(props, "cone_length")
box.separator()
box.prop(props, "color_outer")
box.prop(props, "color_inner")
layout.separator()
# 切離しボタン(現在のアクティブオブジェクトが対象の場合のみ有効)
if obj and obj.name.startswith(props.obj_name):
layout.operator(f"object.{PREFIX}_detach", icon='UNLINKED')
else:
layout.label(text="※生成したオブジェクトを選択して調整", icon='INFO')
class PT_system_panel(bpy.types.Panel):
bl_label = "システム"
bl_idname = f"{PREFIX.upper()}_PT_system"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.operator(f"object.{PREFIX}_copy_settings", icon='COPYDOWN')
layout.separator()
layout.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')
# ===================================
# 登録・解除
# ===================================
classes = [
Properties_Addon,
OT_create_pierced_sphere,
OT_detach_object,
OT_copy_settings,
OT_remove_addon,
PT_main_panel,
PT_system_panel,
]
def register():
for c in classes:
bpy.utils.register_class(c)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=Properties_Addon))
def unregister():
for c in reversed(classes):
bpy.utils.unregister_class(c)
if hasattr(bpy.types.Scene, f"{PREFIX}_props"):
delattr(bpy.types.Scene, f"{PREFIX}_props")
if __name__ == "__main__":
register()