
小数4桁 対応
# ========================================================================
# Blender 5.0+ Rail Generator (with Links, Detach & Remove)
# 個別線路レール + 原点斜線 + 角度/距離/通過点(小数4桁) + リンク&削除
# ========================================================================
bl_info = {
"name": "Rail Generator & Links",
"author": "zionad",
"version": (1, 6, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Rail Gen",
"description": "5本の線路レール・斜線生成、角度・距離・通過点のUI表示・コピー(小数4桁)、リンク・削除パネル",
"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.0000 # x = -10.0 ~ +10.0
INIT_RAY_RADIUS = 0.0400 # 斜線の太さ(半径)
INIT_RAY_COLOR = (0.00, 1.00, 0.00) # 斜線の色 (Green)
# 5本の線路レールの初期値(白・太さ0.15・表示ON・斜線ON)
INIT_RAILS_DATA = {
"r100": {"radius": 0.1500, "color": (1.00, 1.00, 1.00), "visible": True, "ray_l": True, "ray_r": True},
"r10": {"radius": 0.1500, "color": (1.00, 1.00, 1.00), "visible": True, "ray_l": True, "ray_r": True},
"r5": {"radius": 0.1500, "color": (1.00, 1.00, 1.00), "visible": True, "ray_l": True, "ray_r": True},
"r1": {"radius": 0.1500, "color": (1.00, 1.00, 1.00), "visible": True, "ray_l": True, "ray_r": True},
"r0": {"radius": 0.1500, "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.0001, 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.0001, 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.0001, 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.1500, min=0.0001, precision=4, 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.0001, precision=4, update=update_rails,
description="すべてのレールの長さ(X軸方向)"
)
ray_radius: bpy.props.FloatProperty(
name="斜線の太さ", default=INIT_RAY_RADIUS, min=0.0001, precision=4, 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()
label_info: bpy.props.StringProperty(default="数値")
def execute(self, context):
context.window_manager.clipboard = self.value
self.report({'INFO'}, f"{self.label_info} [{self.value}] をコピーしました")
return {'FINISHED'}
class RAIL_GEN_OT_toggle_all_rays(bpy.types.Operator):
bl_idname = "rail_gen.toggle_all_rays"
bl_label = "全斜線の表示切替"
bl_description = "すべてのレールの左右斜線を一括で表示/非表示にします"
bl_options = {'REGISTER', 'UNDO'}
state: bpy.props.BoolProperty(default=True)
def execute(self, context):
props = context.scene.rail_gen_props
for cfg in RAIL_CONFIGS:
r_prop = getattr(props, cfg["key"])
r_prop.ray_l = self.state
r_prop.ray_r = self.state
update_rails(None, context)
msg = "全斜線を表示しました" if self.state else "全斜線を非表示にしました"
self.report({'INFO'}, msg)
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:.4f}",
f"INIT_RAY_RADIUS = {props.ray_radius:.4f}",
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:.4f}, '
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 (すべて小数4桁で表示・コピー)
# ========================================================================
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="")
# 斜線の一括表示・非表示ボタン
row_all = box_top.row(align=True)
op_on = row_all.operator("rail_gen.toggle_all_rays", text="全斜線 ON", icon='HIDE_OFF')
op_on.state = True
op_off = row_all.operator("rail_gen.toggle_all_rays", text="全斜線 OFF", icon='HIDE_ON')
op_off.state = False
layout.separator()
# --- 個別線路 & 斜線設定 ---
box = layout.box()
box.label(text="5本の個別レール & 斜線解析 (小数4桁)", 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="太さ")
# --- 端点角度 & 端点距離の計算 (小数4桁) ---
deg_left = math.degrees(math.atan2(y_val, -half_l))
deg_right = math.degrees(math.atan2(y_val, +half_l))
dist_end = math.hypot(half_l, y_val)
# --- 左斜線ブロック ---
sub_col = item_box.column(align=True)
row_l1 = sub_col.row(align=True)
icon_l = 'HIDE_OFF' if r.ray_l else 'HIDE_ON'
row_l1.prop(r, "ray_l", text="", icon=icon_l)
row_l1.label(text=f"左角: {deg_left:8.4f}°")
op_la = row_l1.operator("rail_gen.copy_value", text="角コピー", icon='COPYDOWN')
op_la.value = f"{deg_left:.4f}"
op_la.label_info = f"{cfg['label']} 左角度"
row_l2 = sub_col.row(align=True)
row_l2.label(text=f" 距離: {dist_end:8.4f}")
op_ld = row_l2.operator("rail_gen.copy_value", text="距離コピー", icon='COPYDOWN')
op_ld.value = f"{dist_end:.4f}"
op_ld.label_info = f"{cfg['label']} 端点距離"
# --- 右斜線ブロック ---
row_r1 = sub_col.row(align=True)
icon_r = 'HIDE_OFF' if r.ray_r else 'HIDE_ON'
row_r1.prop(r, "ray_r", text="", icon=icon_r)
row_r1.label(text=f"右角: {deg_right:8.4f}°")
op_ra = row_r1.operator("rail_gen.copy_value", text="角コピー", icon='COPYDOWN')
op_ra.value = f"{deg_right:.4f}"
op_ra.label_info = f"{cfg['label']} 右角度"
row_r2 = sub_col.row(align=True)
row_r2.label(text=f" 距離: {dist_end:8.4f}")
op_rd = row_r2.operator("rail_gen.copy_value", text="距離コピー", icon='COPYDOWN')
op_rd.value = f"{dist_end:.4f}"
op_rd.label_info = f"{cfg['label']} 端点距離"
# --- 手前線路の通過点(X座標 & 原点からの距離) ---
if y_val > 0:
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="斜線の手前通過位置 & 距離 (小数4桁):", icon='CON_TRACKTO')
for oy in other_y_list:
pass_x = (half_l / y_val) * oy
pass_dist = math.hypot(pass_x, oy)
oy_str = f"{int(oy)}" if oy.is_integer() else f"{oy:g}"
col_p = pass_box.column(align=True)
# 1行目: 通過X座標
row_p1 = col_p.row(align=True)
row_p1.label(text=f"Y={oy_str:2s} : X=±{pass_x:.4f}")
op_px = row_p1.operator("rail_gen.copy_value", text="Xコピー", icon='COPYDOWN')
op_px.value = f"{pass_x:.4f}"
op_px.label_info = f"{cfg['label']}→Y={oy_str} 通過X"
# 2行目: 原点からの距離
row_p2 = col_p.row(align=True)
row_p2.label(text=f" 距離={pass_dist:.4f}")
op_pd = row_p2.operator("rail_gen.copy_value", text="距離コピー", icon='COPYDOWN')
op_pd.value = f"{pass_dist:.4f}"
op_pd.label_info = f"{cfg['label']}→Y={oy_str} 通過距離"
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_toggle_all_rays,
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)を維持し、
アドオンの無効化や再読み込みが何度行われても安全に終了できるようにすること。
"""
小数2桁
# ========================================================================
# Blender 5.0+ Rail Generator (with Links, Detach & Remove)
# 個別線路レール + 原点斜線 + 角度/距離/通過点UI解析・コピー + リンク&削除
# ========================================================================
bl_info = {
"name": "Rail Generator & Links",
"author": "zionad",
"version": (1, 5, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Rail Gen",
"description": "5本の線路レール・斜線生成、角度・距離・通過点の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()
label_info: bpy.props.StringProperty(default="数値")
def execute(self, context):
context.window_manager.clipboard = self.value
self.report({'INFO'}, f"{self.label_info} [{self.value}] をコピーしました")
return {'FINISHED'}
class RAIL_GEN_OT_toggle_all_rays(bpy.types.Operator):
bl_idname = "rail_gen.toggle_all_rays"
bl_label = "全斜線の表示切替"
bl_description = "すべてのレールの左右斜線を一括で表示/非表示にします"
bl_options = {'REGISTER', 'UNDO'}
state: bpy.props.BoolProperty(default=True)
def execute(self, context):
props = context.scene.rail_gen_props
for cfg in RAIL_CONFIGS:
r_prop = getattr(props, cfg["key"])
r_prop.ray_l = self.state
r_prop.ray_r = self.state
update_rails(None, context)
msg = "全斜線を表示しました" if self.state else "全斜線を非表示にしました"
self.report({'INFO'}, msg)
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="")
# 斜線の一括表示・非表示ボタン
row_all = box_top.row(align=True)
op_on = row_all.operator("rail_gen.toggle_all_rays", text="全斜線 ON", icon='HIDE_OFF')
op_on.state = True
op_off = row_all.operator("rail_gen.toggle_all_rays", text="全斜線 OFF", icon='HIDE_ON')
op_off.state = False
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))
dist_end = math.hypot(half_l, y_val)
# --- 左斜線ブロック ---
sub_col = item_box.column(align=True)
row_l1 = sub_col.row(align=True)
icon_l = 'HIDE_OFF' if r.ray_l else 'HIDE_ON'
row_l1.prop(r, "ray_l", text="", icon=icon_l)
row_l1.label(text=f"左角: {deg_left:6.2f}°")
op_la = row_l1.operator("rail_gen.copy_value", text="角コピー", icon='COPYDOWN')
op_la.value = f"{deg_left:.2f}"
op_la.label_info = f"{cfg['label']} 左角度"
row_l2 = sub_col.row(align=True)
row_l2.label(text=f" 距離: {dist_end:6.2f}")
op_ld = row_l2.operator("rail_gen.copy_value", text="距離コピー", icon='COPYDOWN')
op_ld.value = f"{dist_end:.2f}"
op_ld.label_info = f"{cfg['label']} 端点距離"
# --- 右斜線ブロック ---
row_r1 = sub_col.row(align=True)
icon_r = 'HIDE_OFF' if r.ray_r else 'HIDE_ON'
row_r1.prop(r, "ray_r", text="", icon=icon_r)
row_r1.label(text=f"右角: {deg_right:6.2f}°")
op_ra = row_r1.operator("rail_gen.copy_value", text="角コピー", icon='COPYDOWN')
op_ra.value = f"{deg_right:.2f}"
op_ra.label_info = f"{cfg['label']} 右角度"
row_r2 = sub_col.row(align=True)
row_r2.label(text=f" 距離: {dist_end:6.2f}")
op_rd = row_r2.operator("rail_gen.copy_value", text="距離コピー", icon='COPYDOWN')
op_rd.value = f"{dist_end:.2f}"
op_rd.label_info = f"{cfg['label']} 端点距離"
# --- 手前線路の通過点(X座標 & 原点からの距離) ---
if y_val > 0:
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="斜線の手前通過位置 & 距離:", icon='CON_TRACKTO')
for oy in other_y_list:
# 直線 x = (half_l / y_val) * y による通過位置と距離
pass_x = (half_l / y_val) * oy
pass_dist = math.hypot(pass_x, oy)
oy_str = f"{int(oy)}" if oy.is_integer() else f"{oy:g}"
col_p = pass_box.column(align=True)
# 1行目: 通過X座標
row_p1 = col_p.row(align=True)
row_p1.label(text=f"Y={oy_str:2s} : X=±{pass_x:.2f}")
op_px = row_p1.operator("rail_gen.copy_value", text="Xコピー", icon='COPYDOWN')
op_px.value = f"{pass_x:.2f}"
op_px.label_info = f"{cfg['label']}→Y={oy_str} 通過X"
# 2行目: 原点からの距離
row_p2 = col_p.row(align=True)
row_p2.label(text=f" 距離={pass_dist:.2f}")
op_pd = row_p2.operator("rail_gen.copy_value", text="距離コピー", icon='COPYDOWN')
op_pd.value = f"{pass_dist:.2f}"
op_pd.label_info = f"{cfg['label']}→Y={oy_str} 通過距離"
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_toggle_all_rays,
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)を維持し、
アドオンの無効化や再読み込みが何度行われても安全に終了できるようにすること。
"""