rapture_20260903182001.png

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()