20260604 blender million

要件定義

テスト

rapture_20260708123644.png

Einstein 氏の さぼり 290 AI解説 AI Studio 「カメラアイを情報の収束点(=重力の底)」

bl_info = {
    "name": "最適 長方形生成ツール (Blender 5完全対応版)",
    "author": "Your Name",
    "version": (2, 5),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "Blender 5.x専用 / 新アニメーションシステム対応・確実動作版",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import time

# =========================================================================
# 【設定・定数定義】
# =========================================================================
TAB_NAME        = "最適ツール"
PANEL_TITLE     = "最適"
PREFIX_NAME     = "saiteki_rect"

COLLECTION_MAIN = "Main_Collection"
COLLECTION_SUB  = "Sub_Collection"

OBJ_NAME_TOP    = "Rectangle_Top"
OBJ_NAME_BOTTOM = "Rectangle_Bottom"

INIT_TOP_WIDTH  = 2.0
INIT_TOP_HEIGHT = 1.0
INIT_TOP_DEPTH  = 0.2
INIT_TOP_COLOR  = (0.8, 0.2, 0.2, 1.0)
INIT_TOP_LOC    = (0.0, 0.0, 0.0)
INIT_TOP_ROT    = (0.0, 0.0, 0.0)

INIT_BOT_WIDTH  = 2.0
INIT_BOT_HEIGHT = 1.0
INIT_BOT_DEPTH  = 0.2
INIT_BOT_COLOR  = (0.2, 0.4, 0.8, 1.0)
INIT_BOT_LOC    = (0.0, 0.0, 0.0)
INIT_BOT_ROT    = (0.0, 0.0, 0.0)

# アニメーション初期パラメータ
INIT_ANIM_X_MIN = -10.0
INIT_ANIM_X_MAX = 10.0
INIT_TOP_ANIM_SPEED = 60     
INIT_BOT_ANIM_SPEED = -120   

END_FRAME = 250 

LINK_URL        = "<https://www.blender.org/>"

# =========================================================================
# 【システム / 内部関数】
# =========================================================================
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")

def setup_collections():
    main_coll = bpy.data.collections.get(COLLECTION_MAIN)
    if not main_coll:
        main_coll = bpy.data.collections.new(COLLECTION_MAIN)
        bpy.context.scene.collection.children.link(main_coll)
        
    sub_coll = bpy.data.collections.get(COLLECTION_SUB)
    if not sub_coll:
        sub_coll = bpy.data.collections.new(COLLECTION_SUB)
        main_coll.children.link(sub_coll)
    elif sub_coll.name not in main_coll.children.keys():
        main_coll.children.link(sub_coll)
        
    return sub_coll

def create_rectangle_base(name, color, w, d, h, is_top):
    mesh_name = name + "_mesh"
    mesh = bpy.data.meshes.get(mesh_name)
    
    x, y, z = w / 2.0, d / 2.0, h
    if is_top:
        coords = [(-x, -y, 0), (x, -y, 0), (x, y, 0), (-x, y, 0),
                  (-x, -y, z), (x, -y, z), (x, y, z), (-x, y, z)]
    else:
        coords = [(-x, -y, -z), (x, -y, -z), (x, y, -z), (-x, y, -z),
                  (-x, -y, 0), (x, -y, 0), (x, y, 0), (-x, y, 0)]
    faces = [(0, 1, 2, 3), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)]
    
    if not mesh:
        mesh = bpy.data.meshes.new(mesh_name)
        mesh.from_pydata(coords, [], faces)
    else:
        if len(mesh.vertices) == 8:
            for i, v in enumerate(mesh.vertices): v.co = coords[i]
            
    bm = bmesh.new()
    bm.from_mesh(mesh)
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.get(name)
    if not obj: 
        obj = bpy.data.objects.new(name, mesh)
    else: 
        obj.data = mesh
    
    mat_name = name + "_mat"
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(name=mat_name)
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
    
    if len(obj.data.materials) == 0: 
        obj.data.materials.append(mat)
    else: 
        obj.data.materials[0] = mat
        
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf: 
        bsdf.inputs['Base Color'].default_value = color
    return obj

def ensure_objects_exist(props):
    sub_coll = setup_collections()
    
    top_obj = bpy.data.objects.get(OBJ_NAME_TOP)
    if not top_obj or top_obj.name not in sub_coll.objects:
        top_obj = create_rectangle_base(OBJ_NAME_TOP, props.top_color, props.top_width, props.top_depth, props.top_height, True)
        if top_obj.name not in sub_coll.objects: sub_coll.objects.link(top_obj)
        
    bot_obj = bpy.data.objects.get(OBJ_NAME_BOTTOM)
    if not bot_obj or bot_obj.name not in sub_coll.objects:
        bot_obj = create_rectangle_base(OBJ_NAME_BOTTOM, props.bot_color, props.bot_width, props.bot_depth, props.bot_height, False)
        if bot_obj.name not in sub_coll.objects: sub_coll.objects.link(bot_obj)

# =========================================================================
# 【リアルタイム更新コールバック】
# =========================================================================
def update_shapes(self, context):
    ensure_objects_exist(self)
    top_obj = bpy.data.objects.get(OBJ_NAME_TOP)
    if top_obj and top_obj.data and len(top_obj.data.vertices) == 8:
        x, y, z = self.top_width / 2.0, self.top_depth / 2.0, self.top_height
        coords = [(-x, -y, 0), (x, -y, 0), (x, y, 0), (-x, y, 0), (-x, -y, z), (x, -y, z), (x, y, z), (-x, y, z)]
        for i, v in enumerate(top_obj.data.vertices): v.co = coords[i]
        top_obj.data.update()
            
    bot_obj = bpy.data.objects.get(OBJ_NAME_BOTTOM)
    if bot_obj and bot_obj.data and len(bot_obj.data.vertices) == 8:
        x, y, z = self.bot_width / 2.0, self.bot_depth / 2.0, self.bot_height
        coords = [(-x, -y, -z), (x, -y, -z), (x, y, -z), (-x, y, -z), (-x, -y, 0), (x, -y, 0), (x, y, 0), (-x, y, 0)]
        for i, v in enumerate(bot_obj.data.vertices): v.co = coords[i]
        bot_obj.data.update()

def update_transforms(self, context):
    ensure_objects_exist(self)
    top_obj = bpy.data.objects.get(OBJ_NAME_TOP)
    if top_obj:
        if self.anim_use and self.top_anim_speed != 0:
            top_obj.location[1], top_obj.location[2] = self.top_loc[1], self.top_loc[2]
        else:
            top_obj.location = self.top_loc
        top_obj.rotation_euler = self.top_rot
        
    bot_obj = bpy.data.objects.get(OBJ_NAME_BOTTOM)
    if bot_obj:
        if self.anim_use and self.bot_anim_speed != 0:
            bot_obj.location[1], bot_obj.location[2] = self.bot_loc[1], self.bot_loc[2]
        else:
            bot_obj.location = self.bot_loc
        bot_obj.rotation_euler = self.bot_rot

def update_materials(self, context):
    ensure_objects_exist(self)
    top_obj = bpy.data.objects.get(OBJ_NAME_TOP)
    if top_obj and top_obj.data.materials:
        bsdf = top_obj.data.materials[0].node_tree.nodes.get("Principled BSDF")
        if bsdf: bsdf.inputs['Base Color'].default_value = self.top_color
            
    bot_obj = bpy.data.objects.get(OBJ_NAME_BOTTOM)
    if bot_obj and bot_obj.data.materials:
        bsdf = bot_obj.data.materials[0].node_tree.nodes.get("Principled BSDF")
        if bsdf: bsdf.inputs['Base Color'].default_value = self.bot_color

# =========================================================================
# 【Blender 5 新アクション対応・確実なアニメーション計算】
# =========================================================================
def setup_object_animation(context, obj_name, speed, loc, is_anim_use, anim_x_min, anim_x_max):
    obj = bpy.data.objects.get(obj_name)
    if not obj: return
    
    # 既存アニメーションをクリア
    if obj.animation_data:
        old_action = obj.animation_data.action
        obj.animation_data_clear()
        if old_action and old_action.users == 0:
            bpy.data.actions.remove(old_action)
            
    if is_anim_use and speed != 0:
        abs_speed = abs(speed)
        
        obj.location[1], obj.location[2] = loc[1], loc[2]
        
        start_x = anim_x_min if speed > 0 else anim_x_max
        end_x = anim_x_max if speed > 0 else anim_x_min
        
        current_frame = 1
        current_x = start_x
        direction = 1 
        
        # --- 最強の回避策 ---
        # 後からカーブを探すのではなく、「キーを打つ時のデフォルト設定」をリニア(等速)にしてしまう
        # これにより、Blender 5 のAction内部構造がどう変わっていてもエラーが出ません。
        old_interp = context.preferences.edit.keyframe_new_interpolation_type
        context.preferences.edit.keyframe_new_interpolation_type = 'LINEAR'
        
        while current_frame <= END_FRAME + abs_speed:
            obj.location[0] = current_x
            obj.keyframe_insert(data_path="location", index=0, frame=current_frame)
            
            current_frame += abs_speed
            current_x = end_x if direction == 1 else start_x
            direction *= -1

        # システム設定を元の状態に戻す
        context.preferences.edit.keyframe_new_interpolation_type = old_interp
            
        if obj.animation_data and obj.animation_data.action:
            # Actionデータの更新フラグだけ立てておく
            if hasattr(obj.animation_data.action, "update_tag"):
                obj.animation_data.action.update_tag()
                
    else:
        obj.location = loc

def update_animation(self, context):
    ensure_objects_exist(self)

    # context を渡すように変更
    setup_object_animation(
        context, OBJ_NAME_TOP, self.top_anim_speed, self.top_loc, 
        self.anim_use, self.anim_x_min, self.anim_x_max
    )
    
    setup_object_animation(
        context, OBJ_NAME_BOTTOM, self.bot_anim_speed, self.bot_loc, 
        self.anim_use, self.anim_x_min, self.anim_x_max
    )

    context.scene.frame_start = 1
    context.scene.frame_end = END_FRAME

    context.scene.frame_set(1)
    context.view_layer.update()

    for area in context.screen.areas:
        if area.type in {'VIEW_3D', 'DOPESHEET_EDITOR', 'GRAPH_EDITOR', 'TIMELINE'}:
            area.tag_redraw()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class SaitekiProperties(bpy.types.PropertyGroup):
    anim_use: bpy.props.BoolProperty(name="アニメーション有効化", default=False, update=update_animation)
    anim_x_min: bpy.props.FloatProperty(name="開始 X", default=INIT_ANIM_X_MIN, update=update_animation)
    anim_x_max: bpy.props.FloatProperty(name="終了 X", default=INIT_ANIM_X_MAX, update=update_animation)
    top_anim_speed: bpy.props.IntProperty(name="上側の速度", default=INIT_TOP_ANIM_SPEED, update=update_animation)
    bot_anim_speed: bpy.props.IntProperty(name="下側の速度", default=INIT_BOT_ANIM_SPEED, update=update_animation)

    top_width: bpy.props.FloatProperty(name="幅 (X)", default=INIT_TOP_WIDTH, min=0.01, update=update_shapes)
    top_height: bpy.props.FloatProperty(name="高さ (Z)", default=INIT_TOP_HEIGHT, min=0.01, update=update_shapes)
    top_depth: bpy.props.FloatProperty(name="厚み (Y)", default=INIT_TOP_DEPTH, min=0.01, update=update_shapes)
    top_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=4, default=INIT_TOP_COLOR, min=0.0, max=1.0, update=update_materials)
    top_loc: bpy.props.FloatVectorProperty(name="作成位置", subtype='TRANSLATION', size=3, default=INIT_TOP_LOC, update=update_transforms)
    top_rot: bpy.props.FloatVectorProperty(name="回転", subtype='EULER', unit='ROTATION', size=3, default=INIT_TOP_ROT, update=update_transforms)
    
    bot_width: bpy.props.FloatProperty(name="幅 (X)", default=INIT_BOT_WIDTH, min=0.01, update=update_shapes)
    bot_height: bpy.props.FloatProperty(name="高さ (Z)", default=INIT_BOT_HEIGHT, min=0.01, update=update_shapes)
    bot_depth: bpy.props.FloatProperty(name="厚み (Y)", default=INIT_BOT_DEPTH, min=0.01, update=update_shapes)
    bot_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=4, default=INIT_BOT_COLOR, min=0.0, max=1.0, update=update_materials)
    bot_loc: bpy.props.FloatVectorProperty(name="作成位置", subtype='TRANSLATION', size=3, default=INIT_BOT_LOC, update=update_transforms)
    bot_rot: bpy.props.FloatVectorProperty(name="回転", subtype='EULER', unit='ROTATION', size=3, default=INIT_BOT_ROT, update=update_transforms)

# =========================================================================
# 【オペレーター】
# =========================================================================
class SAITEKI_OT_create_rects(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_rects"
    bl_label = "オブジェクトを作成"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        ensure_objects_exist(props)
        update_shapes(props, context)
        update_transforms(props, context)
        update_materials(props, context)
        update_animation(props, context)
        self.report({'INFO'}, "オブジェクトを作成(または復元)しました。")
        return {'FINISHED'}

class SAITEKI_OT_reset_rects(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_reset_rects"
    bl_label = "初期値にリセット"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        props.anim_use = False
        props.anim_x_min, props.anim_x_max = INIT_ANIM_X_MIN, INIT_ANIM_X_MAX
        props.top_anim_speed, props.bot_anim_speed = INIT_TOP_ANIM_SPEED, INIT_BOT_ANIM_SPEED
        props.top_width, props.top_height, props.top_depth = INIT_TOP_WIDTH, INIT_TOP_HEIGHT, INIT_TOP_DEPTH
        props.top_color = INIT_TOP_COLOR
        props.top_loc, props.top_rot = INIT_TOP_LOC, INIT_TOP_ROT
        props.bot_width, props.bot_height, props.bot_depth = INIT_BOT_WIDTH, INIT_BOT_HEIGHT, INIT_BOT_DEPTH
        props.bot_color = INIT_BOT_COLOR
        props.bot_loc, props.bot_rot = INIT_BOT_LOC, INIT_BOT_ROT
        ensure_objects_exist(props)
        update_shapes(props, context)
        update_transforms(props, context)
        update_materials(props, context)
        update_animation(props, context)
        return {'FINISHED'}

class SAITEKI_OT_view_selected(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_view_selected"
    bl_label = "選択オブジェクトを画面移動で表示"
    def execute(self, context):
        bpy.ops.view3d.view_selected()
        return {'FINISHED'}

class SAITEKI_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

class SAITEKI_OT_detach_objects(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_detach_objects"
    bl_label = "オブジェクトを切り離す"
    bl_description = "現在のオブジェクトの名前を変更し、ツールの制御下から独立させます"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        timestamp = str(int(time.time()))[-4:]
        count = 0
        for obj_name in [OBJ_NAME_TOP, OBJ_NAME_BOTTOM]:
            obj = bpy.data.objects.get(obj_name)
            if obj:
                obj.name = f"{obj_name}_Fixed_{timestamp}"
                count += 1
                
        if count > 0:
            self.report({'INFO'}, f"{count}個のオブジェクトをツールから切り離しました。")
        else:
            self.report({'WARNING'}, "切り離す対象のオブジェクトが見つかりません。")
        return {'FINISHED'}

# =========================================================================
# 【UIパネル】
# =========================================================================
class SAITEKI_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
    bl_label = PANEL_TITLE
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        
        layout.operator(f"view3d.{PREFIX_SAFE}_view_selected", text="選択オブジェクトを画面移動で表示", icon='ZOOM_SELECTED')
        layout.separator(factor=1.5)
        
        box_create = layout.box()
        box_create.operator(f"object.{PREFIX_SAFE}_create_rects", text="オブジェクトを作成", icon='MESH_CUBE')
        box_create.operator(f"object.{PREFIX_SAFE}_reset_rects", text="初期値にリセット", icon='FILE_REFRESH')
        layout.separator(factor=1.5)
        
        box_anim = layout.box()
        box_anim.label(text="平行移動アニメ ( -10 から 10 の往復 )", icon='ACTION_TWEAK')
        box_anim.prop(props, "anim_use", toggle=True, icon='PLAY')
        
        if props.anim_use:
            col_anim = box_anim.column(align=True)
            col_anim.prop(props, "anim_x_min")
            col_anim.prop(props, "anim_x_max")
            box_anim.separator()
            col_anim_spd = box_anim.column(align=True)
            col_anim_spd.label(text="速度 (片道フレーム数 / マイナスで逆から出発):")
            col_anim_spd.prop(props, "top_anim_speed", text=f"上側 ({OBJ_NAME_TOP})")
            col_anim_spd.prop(props, "bot_anim_speed", text=f"下側 ({OBJ_NAME_BOTTOM})")
            
        layout.separator(factor=1.5)
        
        box_top = layout.box()
        box_top.label(text=f"上側 ({OBJ_NAME_TOP})", icon='TRIA_UP')
        col_top_size = box_top.column(align=True)
        col_top_size.prop(props, "top_width")
        col_top_size.prop(props, "top_height")
        col_top_size.prop(props, "top_depth")
        box_top.separator()
        col_top_loc = box_top.column(align=True)
        col_top_loc.prop(props, "top_loc")
        box_top.separator()
        col_top_rot = box_top.column(align=True)
        col_top_rot.prop(props, "top_rot")
        box_top.separator()
        box_top.prop(props, "top_color")
        
        layout.separator(factor=1.0)
        
        box_bot = layout.box()
        box_bot.label(text=f"下側 ({OBJ_NAME_BOTTOM})", icon='TRIA_DOWN')
        col_bot_size = box_bot.column(align=True)
        col_bot_size.prop(props, "bot_width")
        col_bot_size.prop(props, "bot_height")
        col_bot_size.prop(props, "bot_depth")
        box_bot.separator()
        col_bot_loc = box_bot.column(align=True)
        col_bot_loc.prop(props, "bot_loc")
        box_bot.separator()
        col_bot_rot = box_bot.column(align=True)
        col_bot_rot.prop(props, "bot_rot")
        box_bot.separator()
        box_bot.prop(props, "bot_color")

        layout.separator(factor=1.5)
        
        box_link = layout.box()
        box_link.operator(f"wm.{PREFIX_SAFE}_open_url", text="リンクパネル", icon='URL').url = LINK_URL
        
        box_del = layout.box()
        box_del.label(text="現在のオブジェクトをアドオンから独立させます", icon='INFO')
        box_del.operator(f"object.{PREFIX_SAFE}_detach_objects", text="オブジェクトを切り離す", icon='UNLINKED')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    SaitekiProperties, SAITEKI_OT_create_rects, SAITEKI_OT_reset_rects,
    SAITEKI_OT_view_selected, SAITEKI_OT_open_url, SAITEKI_OT_detach_objects, SAITEKI_PT_main_panel,
]
def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=SaitekiProperties))

def unregister():
    if hasattr(bpy.types.Scene, f"{PREFIX_SAFE}_props"): delattr(bpy.types.Scene, f"{PREFIX_SAFE}_props")
    for c in reversed(classes):
        try: bpy.utils.unregister_class(c)
        except: pass

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()