# ========================================================================
# Blender 5.0+ Rail Generator (with Links, Detach & Remove)
# 個別線路レール + 原点斜線(Green) + 角度・通過点UI表示&コピー + リンク&削除パネル
# ========================================================================
bl_info = {
"name": "Rail Generator & Links",
"author": "zionad",
"version": (1, 4, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Rail Gen",
"description": "5本の線路レール・原点斜線生成、角度および手前通過X座標のUI表示・コピー、リンク・削除パネル",
"category": "Add Mesh",
}
import bpy
import bmesh
import math
import webbrowser
from mathutils import Matrix
from bpy.app.handlers import persistent
# ========================================================================
# 1. 初期パラメータ(ここをコピーしたコードで書き換え可能)
# ========================================================================
INIT_RAIL_LENGTH = 20.00 # x = -10.0 ~ +10.0
INIT_RAY_RADIUS = 0.04 # 斜線の太さ(半径)
INIT_RAY_COLOR = (0.00, 1.00, 0.00) # 斜線の色 (Green)
# 5本の線路レールの初期値(白・太さ0.15・表示ON・斜線ON)
INIT_RAILS_DATA = {
"r100": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True, "ray_l": True, "ray_r": True},
"r10": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True, "ray_l": True, "ray_r": True},
"r5": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True, "ray_l": True, "ray_r": True},
"r1": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True, "ray_l": True, "ray_r": True},
"r0": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True, "ray_l": True, "ray_r": True},
}
# 5本のレール情報マッピング (キー, Y座標, オブジェクト名, 表示ラベル)
RAIL_CONFIGS = [
{"key": "r100", "y": 100.0, "name": "Rail_Y100", "label": "Y = 100"},
{"key": "r10", "y": 10.0, "name": "Rail_Y10", "label": "Y = 10"},
{"key": "r5", "y": 5.0, "name": "Rail_Y5", "label": "Y = 5"},
{"key": "r1", "y": 1.0, "name": "Rail_Y1", "label": "Y = 1"},
{"key": "r0", "y": 0.0, "name": "Rail_Y0", "label": "Y = 0"},
]
# ========================================================================
# 2. Links データ
# ========================================================================
ADDON_LINKS = {
"20261018 線路レール": "<https://app.notion.com/p/20261018-3fd691cbf90680cca22ddd7cb3c0075c>",
"20261005 円柱作成": "<https://app.notion.com/p/20261005-3ef691cbf906807d9521eeb3fecabcf6>",
"20261003 円錐頂点矢印 と 球体_002": "<https://app.notion.com/p/20261003-3ed691cbf90680a187f7d9c5ea268c2b>",
"338 Sphere Cam": "<https://app.notion.com/p/338-Sphere-Cam-3c0347b3454a80abbb61ff2cd585792c>",
"posfie": "<https://posfie.com/@timekagura/t/zionad2022?sort=0>",
"zionadchat": "<https://x.com/zionadchat>",
"背後霊カメラ 最新版リンク": "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>",
"blender コンテナ 20260713": "<https://note.com/zionadmillion/n/n9a47d88fe7b4>",
"骨格立方体": "<https://app.notion.com/p/351-2017-3cb347b3454a8086b15fec2f69408bd5>",
}
_IS_UPDATING = False
_TIMER_RETRY_COUNT = 0
MAX_TIMER_RETRIES = 30
# ========================================================================
# 3. 処理ロジック (マテリアル & レール・斜線生成 & ビュー設定)
# ========================================================================
def setup_single_material(mat, color):
"""マテリアルのシェーダーノード設定"""
mat.use_nodes = True
tree = mat.node_tree
tree.nodes.clear()
out_node = tree.nodes.new(type="ShaderNodeOutputMaterial")
out_node.location = (400, 0)
bsdf = tree.nodes.new(type="ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = (*color[:3], 1.0)
tree.links.new(bsdf.outputs["BSDF"], out_node.inputs["Surface"])
mat.diffuse_color = (*color[:3], 1.0)
def get_or_create_rail_collection(context):
"""親コレクション Rail_System > サブコレクション Rail を取得・作成"""
scene_col = context.scene.collection
parent_name = "Rail_System"
parent_col = bpy.data.collections.get(parent_name)
if not parent_col:
parent_col = bpy.data.collections.new(parent_name)
scene_col.children.link(parent_col)
elif parent_col.name not in scene_col.children:
scene_col.children.link(parent_col)
sub_name = "Rail"
sub_col = bpy.data.collections.get(sub_name)
if not sub_col:
sub_col = bpy.data.collections.new(sub_name)
parent_col.children.link(sub_col)
elif sub_col.name not in parent_col.children:
if sub_col.name in scene_col.children:
scene_col.children.unlink(sub_col)
parent_col.children.link(sub_col)
return sub_col
def create_or_update_cylinder_line(obj_name, target_x, target_y, radius, mat, is_visible, collection):
"""原点 (0,0,0) から目標座標 (target_x, target_y, 0) への円柱ラインを生成・更新"""
obj = bpy.data.objects.get(obj_name)
if not obj:
mesh = bpy.data.meshes.new(f"{obj_name}_Mesh")
obj = bpy.data.objects.new(obj_name, mesh)
collection.objects.link(obj)
else:
if obj.name not in collection.objects:
collection.objects.link(obj)
obj.hide_set(not is_visible)
obj.hide_render = not is_visible
dist = math.hypot(target_x, target_y)
angle_rad = math.atan2(target_y, target_x)
bm = bmesh.new()
bmesh.ops.create_cone(
bm, cap_ends=True, cap_tris=False, segments=16,
radius1=radius, radius2=radius, depth=max(0.001, dist)
)
rot_y = Matrix.Rotation(math.radians(90.0), 4, 'Y')
rot_z = Matrix.Rotation(angle_rad, 4, 'Z')
bmesh.ops.transform(bm, matrix=rot_z @ rot_y, verts=bm.verts)
bm.to_mesh(obj.data)
bm.free()
obj.data.update()
obj.location = (target_x / 2.0, target_y / 2.0, 0.0)
if not obj.data.materials:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
return obj
def update_rails(self, context):
"""5本の個別線路レールおよび原点斜線を生成・更新する"""
global _IS_UPDATING
if _IS_UPDATING:
return
_IS_UPDATING = True
try:
if not hasattr(context.scene, "rail_gen_props"):
return
props = context.scene.rail_gen_props
rail_col = get_or_create_rail_collection(context)
length = max(0.01, props.length)
half_l = length / 2.0
ray_mat_name = "Rail_Ray_Green_Mat"
ray_mat = bpy.data.materials.get(ray_mat_name)
if not ray_mat:
ray_mat = bpy.data.materials.new(ray_mat_name)
setup_single_material(ray_mat, props.ray_color)
for cfg in RAIL_CONFIGS:
r_prop = getattr(props, cfg["key"])
obj_name = cfg["name"]
y_val = cfg["y"]
# --- 1. レール本体 ---
obj = bpy.data.objects.get(obj_name)
if not obj:
mesh = bpy.data.meshes.new(f"{obj_name}_Mesh")
obj = bpy.data.objects.new(obj_name, mesh)
rail_col.objects.link(obj)
else:
if obj.name not in rail_col.objects:
rail_col.objects.link(obj)
obj.hide_set(not r_prop.visible)
obj.hide_render = not r_prop.visible
radius = max(0.001, r_prop.radius)
bm = bmesh.new()
bmesh.ops.create_cone(
bm, cap_ends=True, cap_tris=False, segments=32,
radius1=radius, radius2=radius, depth=length
)
rot_y = Matrix.Rotation(math.radians(90.0), 4, 'Y')
bmesh.ops.transform(bm, matrix=rot_y, verts=bm.verts)
bm.to_mesh(obj.data)
bm.free()
obj.data.update()
obj.location = (0.0, y_val, 0.0)
mat_name = f"{obj_name}_Mat"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
setup_single_material(mat, r_prop.color)
if not obj.data.materials:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
# --- 2. 原点斜線 ---
create_or_update_cylinder_line(
f"Ray_L_{cfg['key']}", -half_l, y_val,
props.ray_radius, ray_mat, (r_prop.ray_l and r_prop.visible), rail_col
)
create_or_update_cylinder_line(
f"Ray_R_{cfg['key']}", +half_l, y_val,
props.ray_radius, ray_mat, (r_prop.ray_r and r_prop.visible), rail_col
)
finally:
_IS_UPDATING = False
def apply_viewport_and_init_rails():
"""3Dビューポートの設定と初期レールの自動生成 (タイマー実行)"""
global _TIMER_RETRY_COUNT
_TIMER_RETRY_COUNT += 1
wm = getattr(bpy.context, "window_manager", None)
if not wm or not getattr(wm, "windows", None):
if _TIMER_RETRY_COUNT >= MAX_TIMER_RETRIES:
return None
return 0.1
found_view3d = False
for window in wm.windows:
screen = getattr(window, "screen", None)
if not screen:
continue
for area in screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
space.show_region_ui = True
found_view3d = True
for region in area.regions:
if region.type == 'UI' and hasattr(region, "active_panel_category"):
region.active_panel_category = 'Rail Gen'
if found_view3d or _TIMER_RETRY_COUNT >= MAX_TIMER_RETRIES:
try:
update_rails(None, bpy.context)
except Exception:
pass
return None
return 0.1
def schedule_viewport_settings_switch():
"""タイマー重複登録を防止しながら起動処理を予約実行"""
global _TIMER_RETRY_COUNT
_TIMER_RETRY_COUNT = 0
if not bpy.app.timers.is_registered(apply_viewport_and_init_rails):
bpy.app.timers.register(apply_viewport_and_init_rails, first_interval=0.1)
@persistent
def on_blend_load_post(dummy):
"""ファイル読み込み完了時に設定を予約"""
schedule_viewport_settings_switch()
# ========================================================================
# 4. Property Group
# ========================================================================
class SingleRailSettings(bpy.types.PropertyGroup):
radius: bpy.props.FloatProperty(
name="太さ", default=0.15, min=0.001, precision=3, update=update_rails,
description="このレールの太さ(半径)"
)
color: bpy.props.FloatVectorProperty(
name="色", subtype='COLOR', default=(1.0, 1.0, 1.0),
min=0.0, max=1.0, size=3, update=update_rails,
description="このレールの色"
)
visible: bpy.props.BoolProperty(
name="レール表示", default=True, update=update_rails,
description="レールの表示 / 非表示"
)
ray_l: bpy.props.BoolProperty(
name="左斜線", default=True, update=update_rails,
description="原点からレール左端への斜線を表示"
)
ray_r: bpy.props.BoolProperty(
name="右斜線", default=True, update=update_rails,
description="原点からレール右端への斜線を表示"
)
class RailGenProperties(bpy.types.PropertyGroup):
length: bpy.props.FloatProperty(
name="レール全長", default=INIT_RAIL_LENGTH, min=0.1, update=update_rails,
description="すべてのレールの長さ(X軸方向)"
)
ray_radius: bpy.props.FloatProperty(
name="斜線の太さ", default=INIT_RAY_RADIUS, min=0.001, precision=3, update=update_rails,
description="原点からの斜線の半径"
)
ray_color: bpy.props.FloatVectorProperty(
name="斜線色", subtype='COLOR', default=INIT_RAY_COLOR,
min=0.0, max=1.0, size=3, update=update_rails
)
r100: bpy.props.PointerProperty(type=SingleRailSettings)
r10: bpy.props.PointerProperty(type=SingleRailSettings)
r5: bpy.props.PointerProperty(type=SingleRailSettings)
r1: bpy.props.PointerProperty(type=SingleRailSettings)
r0: bpy.props.PointerProperty(type=SingleRailSettings)
# ========================================================================
# 5. Operators
# ========================================================================
class RAIL_GEN_OT_copy_value(bpy.types.Operator):
bl_idname = "rail_gen.copy_value"
bl_label = "数値をコピー"
bl_description = "指定した数値をクリップボードにコピーします"
value: bpy.props.StringProperty()
def execute(self, context):
context.window_manager.clipboard = self.value
self.report({'INFO'}, f"数値 {self.value} をコピーしました")
return {'FINISHED'}
class RAIL_GEN_OT_create(bpy.types.Operator):
bl_idname = "rail_gen.create"
bl_label = "線路を作成 / 更新"
bl_description = "指定したパラメータで5本の線路レールと斜線を生成・更新します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
update_rails(None, context)
self.report({'INFO'}, "線路レール・斜線を生成/更新しました")
return {'FINISHED'}
class RAIL_GEN_OT_detach(bpy.types.Operator):
bl_idname = "rail_gen.detach"
bl_label = "完成Objectとして切離し"
bl_description = "現在の生成レールと斜線を完成品としてアドオン制御から切り離し、マテリアルも完全独立化します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
detached_count = 0
suffix = "_Fixed"
for cfg in RAIL_CONFIGS:
names = [cfg["name"], f"Ray_L_{cfg['key']}", f"Ray_R_{cfg['key']}"]
for obj_name in names:
obj = bpy.data.objects.get(obj_name)
if obj:
obj.name = obj.name + suffix
if obj.data:
obj.data.name = obj.data.name + suffix
for slot in obj.material_slots:
if slot.material:
new_mat = slot.material.copy()
new_mat.name = slot.material.name + suffix
slot.material = new_mat
detached_count += 1
if detached_count > 0:
self.report({'INFO'}, f"{detached_count}個のオブジェクトを完成品として切り離しました")
return {'FINISHED'}
else:
self.report({'WARNING'}, "切り離し対象のオブジェクトがありません")
return {'CANCELLED'}
class RAIL_GEN_OT_copy_code(bpy.types.Operator):
bl_idname = "rail_gen.copy_code"
bl_label = "設定をコード形式でコピー"
bl_description = "現在の数値をPythonの初期値コードとしてクリップボードへコピーします"
def execute(self, context):
props = context.scene.rail_gen_props
lines = [
"# ========================================================================",
"# 1. 初期パラメータ(ここをコピーしたコードで書き換え可能)",
"# ========================================================================",
"",
f"INIT_RAIL_LENGTH = {props.length:.2f}",
f"INIT_RAY_RADIUS = {props.ray_radius:.3f}",
f"INIT_RAY_COLOR = ({props.ray_color[0]:.2f}, {props.ray_color[1]:.2f}, {props.ray_color[2]:.2f})",
"",
"INIT_RAILS_DATA = {",
]
for cfg in RAIL_CONFIGS:
r = getattr(props, cfg["key"])
c = r.color
lines.append(
f' "{cfg["key"]}": {{"radius": {r.radius:.3f}, '
f'"color": ({c[0]:.2f}, {c[1]:.2f}, {c[2]:.2f}), "visible": {r.visible}, '
f'"ray_l": {r.ray_l}, "ray_r": {r.ray_r}}},'
)
lines.append("}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "現在の個別設定をコード形式でコピーしました")
return {'FINISHED'}
class RAIL_GEN_OT_open_url(bpy.types.Operator):
bl_idname = "rail_gen.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url:
webbrowser.open(self.url)
return {'FINISHED'}
class RAIL_GEN_OT_remove_addon(bpy.types.Operator):
bl_idname = "wm.rail_gen_remove_addon"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ========================================================================
# 6. UI Panels
# ========================================================================
class VIEW3D_PT_rail_generator(bpy.types.Panel):
bl_label = "Rail Generator"
bl_order = 0
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Rail Gen'
def draw(self, context):
layout = self.layout
props = context.scene.rail_gen_props
half_l = props.length / 2.0
# --- 全体設定 ---
box_top = layout.box()
box_top.label(text="全体寸法 & 斜線設定", icon='PREFERENCES')
box_top.prop(props, "length")
row_ray = box_top.row(align=True)
row_ray.prop(props, "ray_radius", text="斜線太さ")
row_ray.prop(props, "ray_color", text="")
layout.separator()
# --- 個別線路 & 斜線設定 ---
box = layout.box()
box.label(text="5本の個別レール & 斜線解析", icon='SNAP_EDGE')
for cfg in RAIL_CONFIGS:
r = getattr(props, cfg["key"])
y_val = cfg["y"]
item_box = box.box()
# 1行目: [レール目玉] [Y = 〇〇] [レール色]
row1 = item_box.row(align=True)
icon = 'HIDE_OFF' if r.visible else 'HIDE_ON'
row1.prop(r, "visible", text="", icon=icon)
row1.label(text=cfg["label"])
row1.prop(r, "color", text="")
# 2行目: 太さ
row2 = item_box.row(align=True)
row2.prop(r, "radius", text="太さ")
# --- 角度計算 (小数点2桁) ---
deg_left = math.degrees(math.atan2(y_val, -half_l))
deg_right = math.degrees(math.atan2(y_val, +half_l))
# 3行目: 左斜線設定 (目玉 / 角度表示 / コピーボタン)
sub_col = item_box.column(align=True)
row_l = sub_col.row(align=True)
icon_l = 'HIDE_OFF' if r.ray_l else 'HIDE_ON'
row_l.prop(r, "ray_l", text="", icon=icon_l)
row_l.label(text=f"左角: {deg_left:6.2f}°")
op_l = row_l.operator("rail_gen.copy_value", text="コピー", icon='COPYDOWN')
op_l.value = f"{deg_left:.2f}"
# 4行目: 右斜線設定 (目玉 / 角度表示 / コピーボタン)
row_r = sub_col.row(align=True)
icon_r = 'HIDE_OFF' if r.ray_r else 'HIDE_ON'
row_r.prop(r, "ray_r", text="", icon=icon_r)
row_r.label(text=f"右角: {deg_right:6.2f}°")
op_r = row_r.operator("rail_gen.copy_value", text="コピー", icon='COPYDOWN')
op_r.value = f"{deg_right:.2f}"
# --- 5行目: 手前の各線路(Y座標)を通過する位置の表示とコピー ---
if y_val > 0:
# 自分より手前にある正のY座標を昇順で抽出 (例: y=100なら 1, 5, 10)
other_y_list = [c["y"] for c in RAIL_CONFIGS if 0 < c["y"] < y_val]
other_y_list.sort()
if other_y_list:
pass_box = item_box.box()
pass_box.label(text="斜線の通過 X座標 (|X|):", icon='CON_TRACKTO')
for oy in other_y_list:
# 直線 x = (half_l / y_val) * y による通過位置
pass_x = (half_l / y_val) * oy
row_p = pass_box.row(align=True)
oy_str = f"{int(oy)}" if oy.is_integer() else f"{oy:g}"
row_p.label(text=f"Y={oy_str:2s} : X = ±{pass_x:.2f}")
op_p = row_p.operator("rail_gen.copy_value", text="コピー", icon='COPYDOWN')
op_p.value = f"{pass_x:.2f}"
layout.separator()
layout.operator("rail_gen.create", text="線路を作成 / 更新", icon='FORCE_VORTEX')
layout.operator("rail_gen.detach", text="完成Objectとして切離し", icon='UNLINKED')
# --- コードコピーパネル ---
box_out = layout.box()
box_out.label(text="現在の数値を初期値に", icon='TEXT')
box_out.operator("rail_gen.copy_code", text="コード形式でコピー", icon='COPYDOWN')
class VIEW3D_PT_rail_gen_links(bpy.types.Panel):
bl_label = "Links"
bl_order = 100
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Rail Gen'
def draw(self, context):
layout = self.layout
for name, url in ADDON_LINKS.items():
if name and url:
layout.operator("rail_gen.open_url", text=name, icon='URL').url = url
class VIEW3D_PT_rail_gen_remove(bpy.types.Panel):
bl_label = "Addon Remove"
bl_order = 101
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Rail Gen'
def draw(self, context):
layout = self.layout
layout.operator("wm.rail_gen_remove_addon", icon='CANCEL')
# ========================================================================
# 7. Register / Unregister
# ========================================================================
classes = (
SingleRailSettings,
RailGenProperties,
RAIL_GEN_OT_copy_value,
RAIL_GEN_OT_create,
RAIL_GEN_OT_detach,
RAIL_GEN_OT_copy_code,
RAIL_GEN_OT_open_url,
RAIL_GEN_OT_remove_addon,
VIEW3D_PT_rail_generator,
VIEW3D_PT_rail_gen_links,
VIEW3D_PT_rail_gen_remove,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.rail_gen_props = bpy.props.PointerProperty(type=RailGenProperties)
# 1. ファイル読み込みハンドラーの登録 (二重登録防止)
if on_blend_load_post not in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.append(on_blend_load_post)
# 2. 起動時のビューポート初期化 & レール・斜線自動生成
schedule_viewport_settings_switch()
def unregister():
# 1. 実行中タイマーの安全な解除
if bpy.app.timers.is_registered(apply_viewport_and_init_rails):
try:
bpy.app.timers.unregister(apply_viewport_and_init_rails)
except ValueError:
pass
# 2. ファイル読み込みハンドラーの安全な解除
if on_blend_load_post in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(on_blend_load_post)
# 3. プロパティおよびクラスの解除
if hasattr(bpy.types.Scene, "rail_gen_props"):
del bpy.types.Scene.rail_gen_props
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except Exception:
pass
if __name__ == "__main__":
try:
unregister()
except Exception:
pass
register()
# ========================================================================
# アドオン開発用ひな形(テンプレート)仕様指示プロンプト
# ========================================================================
"""
【AI / 開発者向けプロンプト:本ひな形を基にしたアドオン改修・新規作成ルール】
本スクリプトは、Blender 5.0+ 向けアドオンの標準ひな形(テンプレート)です。
機能を差し替える、または新機能を追加して新しいアドオンを作成する場合は、
以下の「必須保護機能」および「構造ルール」を厳守してください。
1. マテリアルプレビュー自動切り替え機能の維持
- アドオン有効化時、Blender起動時、ファイル読み込み完了時(load_post)に、
3Dビューポートの表示モードを「マテリアルプレビュー」に自動遷移させること。
(API: space.shading.type = 'MATERIAL')
- タイマー(bpy.app.timers)による非同期処理、二重登録防止、
および上限回数(MAX_TIMER_RETRIES)による安全終了ループを維持すること。
2. サイドバー(Nパネル)起動時自動オープンの維持
- 起動時や読み込み時に、3Dビューポートのサイドバーを自動で展開すること。
(API: space.show_region_ui = True)
- サイドバー内のタブ(bl_category)をアドオン専用タブへ自動フォーカスする
処理(region.active_panel_category)を維持すること。
※ 新規作成で bl_category 名を変更した場合は、フォーカス先文字列も同期させること。
3. アドオン削除パネル・機能コードの変更禁止(完全保護)
- パネル最下段(bl_order = 101)の「VIEW3D_PT_rail_gen_remove」
および削除オペレーター「RAIL_GEN_OT_remove_addon」のコードは一切削除・変更しないこと。
- 他の機能拡張の都合で削除処理の構造や配置順序を変えないこと。
4. パネル配置ルール
- メイン操作パネル:上部(bl_order = 0〜)
- Links(関連リンク)パネル:下から2番目(bl_order = 100)
- Addon Remove(アドオン削除)パネル:最下段(bl_order = 101)
5. 登録・解除の安全性(二重登録・解除エラー防止)
- ハンドラー(load_post)登録時の二重登録判定(if ... not in ...)を維持すること。
- unregister() 時のタイマー解除、ハンドラー解除における例外保護(try-except)を維持し、
アドオンの無効化や再読み込みが何度行われても安全に終了できるようにすること。
"""
# ========================================================================
# Blender 5.0+ Rail Generator (with Links, Detach & Remove)
# 個別線路レール生成パネル + 切離し機能 + コピー機能 + リンク&削除パネル
# ========================================================================
bl_info = {
"name": "Rail Generator & Links",
"author": "zionad",
"version": (1, 2, 1),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Rail Gen",
"description": "5本の線路レール個別生成(太さ・色・表示切替)、初期値コードコピー、リンク・削除パネル",
"category": "Add Mesh",
}
import bpy
import bmesh
import math
import webbrowser
from mathutils import Matrix
from bpy.app.handlers import persistent
# ========================================================================
# 1. 初期パラメータ(ここをコピーしたコードで書き換え可能)
# ========================================================================
INIT_RAIL_LENGTH = 20.00 # x = -10.0 ~ +10.0
# 5本の線路レールの初期値(白・太さ0.15・表示ON)
INIT_RAILS_DATA = {
"r100": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True},
"r10": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True},
"r5": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True},
"r1": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True},
"r0": {"radius": 0.15, "color": (1.00, 1.00, 1.00), "visible": True},
}
# 5本のレール情報マッピング (キー, Y座標, オブジェクト名, 表示ラベル)
RAIL_CONFIGS = [
{"key": "r100", "y": 100.0, "name": "Rail_Y100", "label": "Y = 100"},
{"key": "r10", "y": 10.0, "name": "Rail_Y10", "label": "Y = 10"},
{"key": "r5", "y": 5.0, "name": "Rail_Y5", "label": "Y = 5"},
{"key": "r1", "y": 1.0, "name": "Rail_Y1", "label": "Y = 1"},
{"key": "r0", "y": 0.0, "name": "Rail_Y0", "label": "Y = 0"},
]
# ========================================================================
# 2. Links データ
# ========================================================================
ADDON_LINKS = {
"20261018 線路レール": "<https://app.notion.com/p/20261018-3fd691cbf90680cca22ddd7cb3c0075c>",
"20261005 円柱作成": "<https://app.notion.com/p/20261005-3ef691cbf906807d9521eeb3fecabcf6>",
"20261003 円錐頂点矢印 と 球体_002": "<https://app.notion.com/p/20261003-3ed691cbf90680a187f7d9c5ea268c2b>",
"338 Sphere Cam": "<https://app.notion.com/p/338-Sphere-Cam-3c0347b3454a80abbb61ff2cd585792c>",
"posfie": "<https://posfie.com/@timekagura/t/zionad2022?sort=0>",
"zionadchat": "<https://x.com/zionadchat>",
"背後霊カメラ 最新版リンク": "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>",
"blender コンテナ 20260713": "<https://note.com/zionadmillion/n/n9a47d88fe7b4>",
"骨格立方体": "<https://app.notion.com/p/351-2017-3cb347b3454a8086b15fec2f69408bd5>",
}
_IS_UPDATING = False
_TIMER_RETRY_COUNT = 0
MAX_TIMER_RETRIES = 30 # 0.1秒 × 最大30回 (最大3秒でタイムアウト終了)
# ========================================================================
# 3. 処理ロジック (マテリアル & レール生成 & ビュー設定)
# ========================================================================
def setup_single_material(mat, color):
"""マテリアルのシェーダーノード設定"""
mat.use_nodes = True
tree = mat.node_tree
tree.nodes.clear()
out_node = tree.nodes.new(type="ShaderNodeOutputMaterial")
out_node.location = (400, 0)
bsdf = tree.nodes.new(type="ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = (*color[:3], 1.0)
tree.links.new(bsdf.outputs["BSDF"], out_node.inputs["Surface"])
mat.diffuse_color = (*color[:3], 1.0)
def get_or_create_rail_collection(context):
"""親コレクション Rail_System > サブコレクション Rail を取得・作成"""
scene_col = context.scene.collection
parent_name = "Rail_System"
parent_col = bpy.data.collections.get(parent_name)
if not parent_col:
parent_col = bpy.data.collections.new(parent_name)
scene_col.children.link(parent_col)
elif parent_col.name not in scene_col.children:
scene_col.children.link(parent_col)
sub_name = "Rail"
sub_col = bpy.data.collections.get(sub_name)
if not sub_col:
sub_col = bpy.data.collections.new(sub_name)
parent_col.children.link(sub_col)
elif sub_col.name not in parent_col.children:
if sub_col.name in scene_col.children:
scene_col.children.unlink(sub_col)
parent_col.children.link(sub_col)
return sub_col
def update_rails(self, context):
"""5本の個別線路レールを生成・更新する"""
global _IS_UPDATING
if _IS_UPDATING:
return
_IS_UPDATING = True
try:
if not hasattr(context.scene, "rail_gen_props"):
return
props = context.scene.rail_gen_props
rail_col = get_or_create_rail_collection(context)
length = max(0.01, props.length)
for cfg in RAIL_CONFIGS:
r_prop = getattr(props, cfg["key"])
obj_name = cfg["name"]
y_val = cfg["y"]
obj = bpy.data.objects.get(obj_name)
if not obj:
mesh = bpy.data.meshes.new(f"{obj_name}_Mesh")
obj = bpy.data.objects.new(obj_name, mesh)
rail_col.objects.link(obj)
else:
if obj.name not in rail_col.objects:
rail_col.objects.link(obj)
# 表示・非表示の設定
obj.hide_set(not r_prop.visible)
obj.hide_render = not r_prop.visible
# 円柱メッシュの作成 (create_cone で上下半径を等しくする)
radius = max(0.001, r_prop.radius)
bm = bmesh.new()
bmesh.ops.create_cone(
bm, cap_ends=True, cap_tris=False, segments=32,
radius1=radius, radius2=radius, depth=length
)
# デフォルト(Z軸向き)をY軸中心に90度回転させてX軸に平行にする
rot_y = Matrix.Rotation(math.radians(90.0), 4, 'Y')
bmesh.ops.transform(bm, matrix=rot_y, verts=bm.verts)
bm.to_mesh(obj.data)
bm.free()
obj.data.update()
# 位置設定: X中心=0 (x = -length/2 ~ +length/2), 指定Y座標, Z=0
obj.location = (0.0, y_val, 0.0)
# 個別マテリアル設定
mat_name = f"{obj_name}_Mat"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
setup_single_material(mat, r_prop.color)
if not obj.data.materials:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
finally:
_IS_UPDATING = False
def apply_viewport_and_init_rails():
"""3Dビューポートの設定と初期レールの自動生成 (タイマー実行)"""
global _TIMER_RETRY_COUNT
_TIMER_RETRY_COUNT += 1
wm = getattr(bpy.context, "window_manager", None)
if not wm or not getattr(wm, "windows", None):
if _TIMER_RETRY_COUNT >= MAX_TIMER_RETRIES:
return None
return 0.1
found_view3d = False
for window in wm.windows:
screen = getattr(window, "screen", None)
if not screen:
continue
for area in screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
# 1. マテリアルプレビュー表示に設定
space.shading.type = 'MATERIAL'
# 2. サイドバー(Nパネル)を開く
space.show_region_ui = True
found_view3d = True
# サイドバーのアクティブタブを「Rail Gen」にフォーカス
for region in area.regions:
if region.type == 'UI' and hasattr(region, "active_panel_category"):
region.active_panel_category = 'Rail Gen'
# 3Dビューポート準備後に初期レールを生成してタイマー終了
if found_view3d or _TIMER_RETRY_COUNT >= MAX_TIMER_RETRIES:
try:
update_rails(None, bpy.context)
except Exception:
pass
return None
return 0.1
def schedule_viewport_settings_switch():
"""タイマー重複登録を防止しながら起動処理を予約実行"""
global _TIMER_RETRY_COUNT
_TIMER_RETRY_COUNT = 0
if not bpy.app.timers.is_registered(apply_viewport_and_init_rails):
bpy.app.timers.register(apply_viewport_and_init_rails, first_interval=0.1)
@persistent
def on_blend_load_post(dummy):
"""ファイル読み込み完了時に設定を予約"""
schedule_viewport_settings_switch()
# ========================================================================
# 4. Property Group
# ========================================================================
class SingleRailSettings(bpy.types.PropertyGroup):
radius: bpy.props.FloatProperty(
name="太さ", default=0.15, min=0.001, precision=3, update=update_rails,
description="このレールの太さ(半径)"
)
color: bpy.props.FloatVectorProperty(
name="色", subtype='COLOR', default=(1.0, 1.0, 1.0),
min=0.0, max=1.0, size=3, update=update_rails,
description="このレールの色"
)
visible: bpy.props.BoolProperty(
name="表示", default=True, update=update_rails,
description="レールの表示 / 非表示"
)
class RailGenProperties(bpy.types.PropertyGroup):
length: bpy.props.FloatProperty(
name="レール全長", default=INIT_RAIL_LENGTH, min=0.1, update=update_rails,
description="すべてのレールの長さ(X軸方向)"
)
# 5本それぞれの独立した設定プロパティ
r100: bpy.props.PointerProperty(type=SingleRailSettings)
r10: bpy.props.PointerProperty(type=SingleRailSettings)
r5: bpy.props.PointerProperty(type=SingleRailSettings)
r1: bpy.props.PointerProperty(type=SingleRailSettings)
r0: bpy.props.PointerProperty(type=SingleRailSettings)
# ========================================================================
# 5. Operators
# ========================================================================
class RAIL_GEN_OT_create(bpy.types.Operator):
bl_idname = "rail_gen.create"
bl_label = "線路を作成 / 更新"
bl_description = "指定したパラメータで5本の線路レールを生成・更新します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
update_rails(None, context)
self.report({'INFO'}, "線路レールを生成/更新しました")
return {'FINISHED'}
class RAIL_GEN_OT_detach(bpy.types.Operator):
bl_idname = "rail_gen.detach"
bl_label = "完成Objectとして切離し"
bl_description = "現在の生成レールを完成品としてアドオン制御から切り離し、マテリアルも完全独立化します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
detached_count = 0
suffix = "_Fixed"
for cfg in RAIL_CONFIGS:
obj = bpy.data.objects.get(cfg["name"])
if obj:
obj.name = obj.name + suffix
if obj.data:
obj.data.name = obj.data.name + suffix
for slot in obj.material_slots:
if slot.material:
new_mat = slot.material.copy()
new_mat.name = slot.material.name + suffix
slot.material = new_mat
detached_count += 1
if detached_count > 0:
self.report({'INFO'}, f"{detached_count}本のレールを完成品として切り離しました")
return {'FINISHED'}
else:
self.report({'WARNING'}, "切り離し対象のレールオブジェクトがありません")
return {'CANCELLED'}
class RAIL_GEN_OT_copy_code(bpy.types.Operator):
bl_idname = "rail_gen.copy_code"
bl_label = "設定をコード形式でコピー"
bl_description = "現在の5本の数値をPythonの初期値コードとしてクリップボードへコピーします"
def execute(self, context):
props = context.scene.rail_gen_props
lines = [
"# ========================================================================",
"# 1. 初期パラメータ(ここをコピーしたコードで書き換え可能)",
"# ========================================================================",
"",
f"INIT_RAIL_LENGTH = {props.length:.2f}",
"",
"INIT_RAILS_DATA = {",
]
for cfg in RAIL_CONFIGS:
r = getattr(props, cfg["key"])
c = r.color
lines.append(
f' "{cfg["key"]}": {{"radius": {r.radius:.3f}, '
f'"color": ({c[0]:.2f}, {c[1]:.2f}, {c[2]:.2f}), "visible": {r.visible}}},'
)
lines.append("}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "現在の個別設定をコード形式でコピーしました")
return {'FINISHED'}
class RAIL_GEN_OT_open_url(bpy.types.Operator):
bl_idname = "rail_gen.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url:
webbrowser.open(self.url)
return {'FINISHED'}
class RAIL_GEN_OT_remove_addon(bpy.types.Operator):
bl_idname = "wm.rail_gen_remove_addon"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ========================================================================
# 6. UI Panels
# ========================================================================
class VIEW3D_PT_rail_generator(bpy.types.Panel):
bl_label = "Rail Generator"
bl_order = 0
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Rail Gen'
def draw(self, context):
layout = self.layout
props = context.scene.rail_gen_props
# --- 全体設定(長さ) ---
box_top = layout.box()
box_top.label(text="全体設定", icon='PREFERENCES')
box_top.prop(props, "length")
layout.separator()
# --- 個別線路設定(狭いサイドバーでも崩れない2段レイアウト) ---
box = layout.box()
box.label(text="5本の個別レール設定 (Z=0)", icon='SNAP_EDGE')
for cfg in RAIL_CONFIGS:
r = getattr(props, cfg["key"])
item_box = box.box()
# 1行目: [目玉アイコン] [Y = 〇〇] [カラーピッカー]
row1 = item_box.row(align=True)
icon = 'HIDE_OFF' if r.visible else 'HIDE_ON'
row1.prop(r, "visible", text="", icon=icon)
row1.label(text=cfg["label"])
row1.prop(r, "color", text="")
# 2行目: 太さ(スライダー)
row2 = item_box.row(align=True)
row2.prop(r, "radius", text="太さ")
layout.separator()
layout.operator("rail_gen.create", text="線路を作成 / 更新", icon='FORCE_VORTEX')
layout.operator("rail_gen.detach", text="完成Objectとして切離し", icon='UNLINKED')
# --- コードコピーパネル ---
box_out = layout.box()
box_out.label(text="現在の数値を初期値に", icon='TEXT')
box_out.operator("rail_gen.copy_code", text="コード形式でコピー", icon='COPYDOWN')
class VIEW3D_PT_rail_gen_links(bpy.types.Panel):
bl_label = "Links"
bl_order = 100
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Rail Gen'
def draw(self, context):
layout = self.layout
for name, url in ADDON_LINKS.items():
if name and url:
layout.operator("rail_gen.open_url", text=name, icon='URL').url = url
class VIEW3D_PT_rail_gen_remove(bpy.types.Panel):
bl_label = "Addon Remove"
bl_order = 101
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Rail Gen'
def draw(self, context):
layout = self.layout
layout.operator("wm.rail_gen_remove_addon", icon='CANCEL')
# ========================================================================
# 7. Register / Unregister
# ========================================================================
classes = (
SingleRailSettings,
RailGenProperties,
RAIL_GEN_OT_create,
RAIL_GEN_OT_detach,
RAIL_GEN_OT_copy_code,
RAIL_GEN_OT_open_url,
RAIL_GEN_OT_remove_addon,
VIEW3D_PT_rail_generator,
VIEW3D_PT_rail_gen_links,
VIEW3D_PT_rail_gen_remove,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.rail_gen_props = bpy.props.PointerProperty(type=RailGenProperties)
# 1. ファイル読み込みハンドラーの登録 (二重登録防止)
if on_blend_load_post not in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.append(on_blend_load_post)
# 2. 起動時のビューポート初期化 & 5本の線路レール自動生成
schedule_viewport_settings_switch()
def unregister():
# 1. 実行中タイマーの安全な解除
if bpy.app.timers.is_registered(apply_viewport_and_init_rails):
try:
bpy.app.timers.unregister(apply_viewport_and_init_rails)
except ValueError:
pass
# 2. ファイル読み込みハンドラーの安全な解除
if on_blend_load_post in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(on_blend_load_post)
# 3. プロパティおよびクラスの解除
if hasattr(bpy.types.Scene, "rail_gen_props"):
del bpy.types.Scene.rail_gen_props
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except Exception:
pass
if __name__ == "__main__":
try:
unregister()
except Exception:
pass
register()
# ========================================================================
# アドオン開発用ひな形(テンプレート)仕様指示プロンプト
# ========================================================================
"""
【AI / 開発者向けプロンプト:本ひな形を基にしたアドオン改修・新規作成ルール】
本スクリプトは、Blender 5.0+ 向けアドオンの標準ひな形(テンプレート)です。
機能を差し替える、または新機能を追加して新しいアドオンを作成する場合は、
以下の「必須保護機能」および「構造ルール」を厳守してください。
1. マテリアルプレビュー自動切り替え機能の維持
- アドオン有効化時、Blender起動時、ファイル読み込み完了時(load_post)に、
3Dビューポートの表示モードを「マテリアルプレビュー」に自動遷移させること。
(API: space.shading.type = 'MATERIAL')
- タイマー(bpy.app.timers)による非同期処理、二重登録防止、
および上限回数(MAX_TIMER_RETRIES)による安全終了ループを維持すること。
2. サイドバー(Nパネル)起動時自動オープンの維持
- 起動時や読み込み時に、3Dビューポートのサイドバーを自動で展開すること。
(API: space.show_region_ui = True)
- サイドバー内のタブ(bl_category)をアドオン専用タブへ自動フォーカスする
処理(region.active_panel_category)を維持すること。
※ 新規作成で bl_category 名を変更した場合は、フォーカス先文字列も同期させること。
3. アドオン削除パネル・機能コードの変更禁止(完全保護)
- パネル最下段(bl_order = 101)の「VIEW3D_PT_rail_gen_remove」
および削除オペレーター「RAIL_GEN_OT_remove_addon」のコードは一切削除・変更しないこと。
- 他の機能拡張の都合で削除処理の構造や配置順序を変えないこと。
4. パネル配置ルール
- メイン操作パネル:上部(bl_order = 0〜)
- Links(関連リンク)パネル:下から2番目(bl_order = 100)
- Addon Remove(アドオン削除)パネル:最下段(bl_order = 101)
5. 登録・解除の安全性(二重登録・解除エラー防止)
- ハンドラー(load_post)登録時の二重登録判定(if ... not in ...)を維持すること。
- unregister() 時のタイマー解除、ハンドラー解除における例外保護(try-except)を維持し、
アドオンの無効化や再読み込みが何度行われても安全に終了できるようにすること。
"""