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