
bl_info = {
"name": "zionad_ [ 頂点固定・穴あき球体 ]",
"author": "zionad_",
"version": (3, 5, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "頂点(0,0,0)固定の円錐で球体を加工するツール",
"category": "3D View",
}
import bpy
import bmesh
import math
import random
# ===================================
# 識別定義
# ===================================
PREFIX_VAL = "fixed_apex_sphere_v3"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "球体加工"
# 初期値
INIT_OBJ_NAME = "Apex_Fixed_Sphere"
INIT_SPHERE_SIZE = 10.0
INIT_CONE_ANGLE = 45.0
INIT_CONE_LENGTH = 12.0
INIT_COLOR_OUTER = (0.1, 0.1, 0.1, 1.0)
INIT_COLOR_INNER = (0.0, 1.0, 0.5, 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 = bpy.data.objects.new(cutter_name, bpy.data.meshes.new(cutter_name))
context.collection.objects.link(cutter)
cutter.hide_viewport = True
cutter.hide_render = True
# 角度と長さから底面の半径を計算
half_angle_rad = math.radians(props.cone_angle / 2.0)
calc_radius = props.cone_length * math.tan(half_angle_rad)
bm_cone = bmesh.new()
bmesh.ops.create_cone(
bm_cone, cap_ends=True, segments=64,
radius1=calc_radius, radius2=0, depth=props.cone_length
)
# 【重要】頂点を(0,0,0)に固定するための移動
# Blenderの円錐は中心が(0,0,0)で作られるため、高さの半分移動させる
offset = props.cone_length / 2.0
if props.cone_reverse:
# 下向き(Z-)
bmesh.ops.translate(bm_cone, vec=(0, 0, -offset), verts=bm_cone.verts)
else:
# 上向き(Z+)
bmesh.ops.translate(bm_cone, vec=(0, 0, offset), 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 = "Apex_Boolean"
mod = obj.modifiers.get(mod_name) or obj.modifiers.new(mod_name, 'BOOLEAN')
mod.operation = props.bool_type
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 set_mat(index, suffix, color):
name = f"{PREFIX_VAL}_{suffix}"
mat = bpy.data.materials.get(name) or bpy.data.materials.new(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'
obj.data.materials[index] = mat
set_mat(0, "Outer", props.color_sphere)
set_mat(1, "Inner", props.color_cut)
# ===================================
# オペレーター
# ===================================
class OT_create(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_create"
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(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_detach"
bl_label = "アドオンから切り離す"
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 = bpy.data.objects.get(f"{obj.name}_Cutter")
if cutter: bpy.data.objects.remove(cutter, do_unlink=True)
obj.name = f"Fixed_Apex_Obj_{random.randint(100,999)}"
self.report({'INFO'}, "メッシュを確定しました")
return {'FINISHED'}
class OT_copy(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_copy"
bl_label = "設定をコピー"
def execute(self, context):
p = getattr(context.scene, f"{PREFIX}_props")
text = (
f"INIT_SPHERE_SIZE = {p.sphere_size:.4f}\n"
f"INIT_CONE_ANGLE = {p.cone_angle:.4f}\n"
f"INIT_CONE_LENGTH = {p.cone_length:.4f}\n"
f"INIT_BOOL_TYPE = '{p.bool_type}'\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "コピー完了")
return {'FINISHED'}
class OT_remove(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ===================================
# UI / プロパティ
# ===================================
class Props(bpy.types.PropertyGroup):
obj_name: bpy.props.StringProperty(name="名前", default=INIT_OBJ_NAME)
bool_type: bpy.props.EnumProperty(
name="方式",
items=[('DIFFERENCE', "穴あけ", ""), ('INTERSECT', "切出し", "")],
update=update_geometry
)
cone_reverse: bpy.props.BoolProperty(name="向き反転", default=False, update=update_geometry)
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, max=179, update=update_geometry)
cone_length: bpy.props.FloatProperty(name="円錐長さ", default=INIT_CONE_LENGTH, min=0.1, update=update_geometry)
color_sphere: bpy.props.FloatVectorProperty(name="球の色", subtype='COLOR', size=4, default=INIT_COLOR_OUTER, min=0, max=1, update=update_geometry)
color_cut: bpy.props.FloatVectorProperty(name="穴の色", subtype='COLOR', size=4, default=INIT_COLOR_INNER, min=0, max=1, update=update_geometry)
class PT_panel(bpy.types.Panel):
bl_label = "頂点固定・球体加工"
bl_idname = f"{PREFIX.upper()}_PT_panel"
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(OT_create.bl_idname, icon='MESH_UVSPHERE')
layout.separator()
box = layout.box()
box.label(text="Boolean設定", icon='MOD_BOOLEAN')
box.row().prop(props, "bool_type", expand=True)
box.prop(props, "cone_reverse", toggle=True, icon='SORT_ASC' if not props.cone_reverse else 'SORT_DESC')
box = layout.box()
box.label(text="形状 (頂点は0,0,0固定)", icon='MESH_CONE')
box.prop(props, "sphere_size")
box.prop(props, "cone_angle")
box.prop(props, "cone_length")
box = layout.box()
box.label(text="カラー (0-1)", icon='COLOR')
box.prop(props, "color_sphere")
box.prop(props, "color_cut")
layout.separator()
if obj and obj.name.startswith(props.obj_name):
layout.operator(OT_detach.bl_idname, icon='UNLINKED')
layout.separator()
sys = layout.box()
sys.operator(OT_copy.bl_idname, icon='COPYDOWN')
sys.operator(OT_remove.bl_idname, icon='CANCEL')
# ===================================
# 登録
# ===================================
classes = [Props, OT_create, OT_detach, OT_copy, OT_remove, PT_panel]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=Props))
def unregister():
for c in reversed(classes): bpy.utils.unregister_class(c)
delattr(bpy.types.Scene, f"{PREFIX}_props")
if __name__ == "__main__":
register()