builder 作成テスト
bl_info = {
"name": "Observation Planes Extended",
"author": "zionad2022",
"version": (1, 2, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar(N) > Observation Plane",
"description": "2人の観測者による時間の流れ(過去方向)の不一致を可視化するアドオン",
"category": "3D View",
}
# Code Created : 2026_0716_0549
import bpy
import bmesh
import math
import mathutils
import os
import json
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "Observation Planes"
PREFIX_VAL = "ObsPlane"
ADDON_CATEGORY = ADDON_TITLE
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "アドオン専用ページ"
LINK_URL_1 = "URL"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_4 = "blender コンテナ 20260713"
LINK_URL_4 = "<https://note.com/zionadmillion/n/n9a47d88fe7b4>"
LINK_NAME_5 = ""
LINK_URL_5 = ""
# =========================================================
# 6. 初期設定
# =========================================================
# 数値
GREEN_HALF_LENGTH_DEFAULT = 10.0
TRIANGLE_EXTENSION_DEFAULT = 2.0
BLUE_EYE_X_DEFAULT = 0.0
BLUE_EYE_Y_DEFAULT = -10.0
BLUE_EYE_Z_DEFAULT = -10.0
RED_EYE_X_DEFAULT = 0.0
RED_EYE_Y_DEFAULT = 10.0
RED_EYE_Z_DEFAULT = -10.0
PAST_ARROW_LENGTH_DEFAULT = 5.0
ALPHA_VAL_DEFAULT = 0.5
TEXT_SIZE_DEFAULT = 1.0
# 色 (R, G, B, A)
COLOR_GREEN_EDGE = (0.0, 1.0, 0.5, 1.0)
COLOR_BLUE_PLANE = (0.0, 0.5, 1.0, 1.0)
COLOR_RED_PLANE = (1.0, 0.2, 0.2, 1.0)
COLOR_TEXT = (1.0, 1.0, 1.0, 1.0)
# =========================================================
# 7. 共通クラス
# =========================================================
class OBSPLANE_OT_OpenUrl(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "Open URL"
url: bpy.props.StringProperty()
def execute(self, context):
bpy.ops.wm.url_open(url=self.url)
return {'FINISHED'}
class OBSPLANE_OT_Uninstall(bpy.types.Operator):
bl_idname = f"{PREFIX}.uninstall"
bl_label = "アドオンを完全削除 (Unregister)"
bl_description = "このアドオンをシステムから解除し、UIから削除します"
def execute(self, context):
import sys
mod_name = __name__
if mod_name in sys.modules:
unregister()
self.report({'INFO'}, f"【{ADDON_TITLE}】をUnregisterしました。")
return {'FINISHED'}
# =========================================================
# 8. アドオン固有機能 (Utility)
# =========================================================
def get_or_create_collection():
coll_name = f"{PREFIX_VAL}_Collection"
coll = bpy.data.collections.get(coll_name)
if not coll:
coll = bpy.data.collections.new(coll_name)
bpy.context.scene.collection.children.link(coll)
return coll
def get_or_create_material(name, color, alpha):
mat_name = f"{PREFIX_VAL}_{name}"
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'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = alpha
return mat
def create_or_update_mesh(obj_name, vertices, faces, mat):
coll = get_or_create_collection()
full_name = f"{PREFIX_VAL}_{obj_name}"
obj = bpy.data.objects.get(full_name)
if not obj:
mesh = bpy.data.meshes.new(name=full_name)
obj = bpy.data.objects.new(full_name, mesh)
coll.objects.link(obj)
else:
mesh = obj.data
mesh.clear_geometry()
bm = bmesh.new()
for v in vertices:
bm.verts.new(v)
bm.verts.ensure_lookup_table()
if faces:
for f in faces:
bm.faces.new([bm.verts[i] for i in f])
elif len(vertices) == 2:
bm.edges.new((bm.verts[0], bm.verts[1]))
bm.to_mesh(mesh)
bm.free()
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
return obj
def create_or_update_text(obj_name, text_body, loc, mat, size):
coll = get_or_create_collection()
full_name = f"{PREFIX_VAL}_Text_{obj_name}"
obj = bpy.data.objects.get(full_name)
if not obj:
curve = bpy.data.curves.new(type="FONT", name=full_name)
obj = bpy.data.objects.new(full_name, curve)
coll.objects.link(obj)
obj.data.body = text_body
obj.location = loc
obj.data.size = size
obj.data.align_x = 'CENTER'
obj.data.align_y = 'CENTER'
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
obj.rotation_euler = (1.5708, 0, 0)
return obj
def create_arrow(obj_name, start_loc, direction, length, mat):
end_loc = mathutils.Vector(start_loc) + (mathutils.Vector(direction).normalized() * length)
obj = create_or_update_mesh(obj_name, [start_loc, end_loc], [], mat)
return obj
def update_geometry(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
if not props.is_active:
return
gh = props.green_half_length
ext = props.triangle_extension
alpha = props.alpha_val
text_size = props.text_size
v_green_a = mathutils.Vector((-gh, 0, 0))
v_green_b = mathutils.Vector((gh, 0, 0))
v_blue_eye = mathutils.Vector((props.blue_eye_x, props.blue_eye_y, props.blue_eye_z))
v_red_eye = mathutils.Vector((props.red_eye_x, props.red_eye_y, props.red_eye_z))
v_blue_ext_a = v_blue_eye + (v_green_a - v_blue_eye) * ext
v_blue_ext_b = v_blue_eye + (v_green_b - v_blue_eye) * ext
v_red_ext_a = v_red_eye + (v_green_a - v_red_eye) * ext
v_red_ext_b = v_red_eye + (v_green_b - v_red_eye) * ext
mat_green = get_or_create_material("Mat_Green", COLOR_GREEN_EDGE, alpha)
mat_blue = get_or_create_material("Mat_Blue", COLOR_BLUE_PLANE, alpha)
mat_red = get_or_create_material("Mat_Red", COLOR_RED_PLANE, alpha)
mat_text = get_or_create_material("Mat_Text", COLOR_TEXT, 1.0)
create_or_update_mesh("GreenEdge", [v_green_a, v_green_b], [], mat_green)
obj_blue = create_or_update_mesh("BlueTriangle", [v_blue_eye, v_blue_ext_a, v_blue_ext_b], [[0, 1, 2]], mat_blue)
obj_red = create_or_update_mesh("RedTriangle", [v_red_eye, v_red_ext_a, v_red_ext_b], [[0, 1, 2]], mat_red)
obj_blue["PastValue_Target"] = "Blue (Y>0)"
obj_red["PastValue_Target"] = "Red (Y<0)"
if props.show_past_arrow:
x_axis = mathutils.Vector((1, 0, 0))
blue_normal = mathutils.geometry.normal([v_blue_eye, v_blue_ext_b, v_blue_ext_a])
blue_past_dir = x_axis.cross(blue_normal)
if blue_past_dir.y < 0: blue_past_dir = -blue_past_dir
create_arrow("BluePastArrow", (0,0,0), blue_past_dir, props.past_arrow_length, mat_blue)
red_normal = mathutils.geometry.normal([v_red_eye, v_red_ext_b, v_red_ext_a])
red_past_dir = x_axis.cross(red_normal)
if red_past_dir.y > 0: red_past_dir = -red_past_dir
create_arrow("RedPastArrow", (0,0,0), red_past_dir, props.past_arrow_length, mat_red)
else:
create_or_update_mesh("BluePastArrow", [], [], mat_blue)
create_or_update_mesh("RedPastArrow", [], [], mat_red)
if props.show_labels:
create_or_update_text("Label_Blue", "「俺」の観測平面 (青)\nY > 0 が過去", v_blue_eye + mathutils.Vector((0, -2, 0)), mat_text, text_size)
create_or_update_text("Label_Red", "「貴殿」の観測平面 (赤)\nY < 0 が過去", v_red_eye + mathutils.Vector((0, 2, 0)), mat_text, text_size)
create_or_update_text("Label_Green", "共通事象 (現在)", v_green_b + mathutils.Vector((1.5, 0, 0)), mat_text, text_size)
else:
create_or_update_text("Label_Blue", "", (0,0,0), mat_text, text_size)
create_or_update_text("Label_Red", "", (0,0,0), mat_text, text_size)
create_or_update_text("Label_Green", "", (0,0,0), mat_text, text_size)
# =========================================================
# 8. アドオン固有機能 (Property & Operator)
# =========================================================
class OBSPLANE_Properties(bpy.types.PropertyGroup):
is_active: bpy.props.BoolProperty(default=False)
green_half_length: bpy.props.FloatProperty(name="X軸範囲 (Half Length)", default=GREEN_HALF_LENGTH_DEFAULT, update=update_geometry)
triangle_extension: bpy.props.FloatProperty(name="延長率 (Triangle Ext)", default=TRIANGLE_EXTENSION_DEFAULT, min=1.0, max=10.0, update=update_geometry)
blue_eye_x: bpy.props.FloatProperty(name="Blue X", default=BLUE_EYE_X_DEFAULT, update=update_geometry)
blue_eye_y: bpy.props.FloatProperty(name="Blue Y", default=BLUE_EYE_Y_DEFAULT, update=update_geometry)
blue_eye_z: bpy.props.FloatProperty(name="Blue Z", default=BLUE_EYE_Z_DEFAULT, update=update_geometry)
red_eye_x: bpy.props.FloatProperty(name="Red X", default=RED_EYE_X_DEFAULT, update=update_geometry)
red_eye_y: bpy.props.FloatProperty(name="Red Y", default=RED_EYE_Y_DEFAULT, update=update_geometry)
red_eye_z: bpy.props.FloatProperty(name="Red Z", default=RED_EYE_Z_DEFAULT, update=update_geometry)
show_past_arrow: bpy.props.BoolProperty(name="Show Past Arrow", default=True, update=update_geometry)
past_arrow_length: bpy.props.FloatProperty(name="Past Arrow Length", default=PAST_ARROW_LENGTH_DEFAULT, update=update_geometry)
show_labels: bpy.props.BoolProperty(name="Show Labels", default=True, update=update_geometry)
alpha_val: bpy.props.FloatProperty(name="Opacity (透明度)", default=ALPHA_VAL_DEFAULT, min=0.0, max=1.0, update=update_geometry)
text_size: bpy.props.FloatProperty(name="Text Size (文字サイズ)", default=TEXT_SIZE_DEFAULT, min=0.1, max=10.0, update=update_geometry)
class OBSPLANE_OT_Generate(bpy.types.Operator):
bl_idname = f"{PREFIX}.generate"
bl_label = "観測平面を生成"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.is_active = True
update_geometry(self, context)
return {'FINISHED'}
class OBSPLANE_OT_Detach(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach"
bl_label = "アドオンから切り離し"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.is_active = False
coll_name = f"{PREFIX_VAL}_Collection"
coll = bpy.data.collections.get(coll_name)
if coll:
coll.name = f"{PREFIX_VAL}_Detached"
for obj in coll.objects:
obj.name = obj.name.replace(f"{PREFIX_VAL}_", "Detached_")
self.report({'INFO'}, "アドオンから切り離し、独立させました。")
return {'FINISHED'}
# =========================================================
# 9. パネル構成
# =========================================================
class OBSPLANE_PT_MainPanel(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX.upper()}_PT_MainPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX}_props")
layout.operator(f"{PREFIX}.generate", icon='PLAY')
box = layout.box()
box.label(text="【全体調整】")
box.prop(props, "green_half_length")
box.prop(props, "triangle_extension")
box.prop(props, "alpha_val")
box.prop(props, "text_size")
box = layout.box()
box.label(text="【表示切替】")
box.prop(props, "show_past_arrow")
box.prop(props, "show_labels")
box = layout.box()
box.label(text="【眼の座標】")
col = box.column(align=True)
col.prop(props, "blue_eye_y")
col.prop(props, "blue_eye_z")
col.separator()
col.prop(props, "red_eye_y")
col.prop(props, "red_eye_z")
layout.separator()
layout.operator(f"{PREFIX}.detach", icon='UNLINKED')
# --- 最下部構成 (Links) ---
class OBSPLANE_PT_LinksPanel(bpy.types.Panel):
bl_label = "Links"
bl_idname = f"{PREFIX.upper()}_PT_LinksPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_parent_id = f"{PREFIX.upper()}_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
links = [
(LINK_NAME_1, LINK_URL_1),
(LINK_NAME_2, LINK_URL_2),
(LINK_NAME_3, LINK_URL_3),
(LINK_NAME_4, LINK_URL_4),
(LINK_NAME_5, LINK_URL_5)
]
for name, url in links:
if name and url:
layout.operator(f"{PREFIX}.open_url", text=name, icon='URL').url = url
# --- 最下部構成 (Addon Remove) ---
class OBSPLANE_PT_RemovePanel(bpy.types.Panel):
bl_label = "Addon Remove"
bl_idname = f"{PREFIX.upper()}_PT_RemovePanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_parent_id = f"{PREFIX.upper()}_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.label(text="Unregisterを呼び出して解除します", icon='ERROR')
layout.operator(f"{PREFIX}.uninstall", icon='TRASH')
# =========================================================
# 10. register()
# =========================================================
classes = (
OBSPLANE_OT_OpenUrl,
OBSPLANE_OT_Uninstall,
OBSPLANE_Properties,
OBSPLANE_OT_Generate,
OBSPLANE_OT_Detach,
OBSPLANE_PT_MainPanel,
OBSPLANE_PT_LinksPanel,
OBSPLANE_PT_RemovePanel
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=OBSPLANE_Properties))
# =========================================================
# 11. unregister()
# =========================================================
def unregister():
for cls in reversed(classes):
if hasattr(bpy.types, cls.__name__):
bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, f"{PREFIX}_props"):
delattr(bpy.types.Scene, f"{PREFIX}_props")
if __name__ == "__main__":
register()
初期設定読み込み
bl_info = {
"name": "Observation Planes (Relativity Model)",
"author": "zionad2022",
"version": (1, 1, 0),
"blender": (5, 0, 0), # Blender 5 専用
"location": "View3D > Sidebar(N) > Observation Plane",
"description": "2人の観測者による時間の流れ(過去方向)の不一致を可視化するアドオン(拡張交差版)",
"category": "3D View",
}
import bpy
import bmesh
import mathutils
# =========================================================
# 【初期設定・定数定義】 (タイトル・接頭辞など)
# =========================================================
ADDON_TITLE = "Observation Planes"
PREFIX_VAL = "ObsPlane_"
ADDON_CATEGORY = ADDON_TITLE
# =========================================================
# 【初期設定・リンク定義】
# =========================================================
LINK_NAME_1 = "リンクタイトル"
LINK_URL_1 = "URL"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_4 = "blender コンテナ 20260713"
LINK_URL_4 = "<https://note.com/zionadmillion/n/n9a47d88fe7b4>"
LINK_NAME_5 = ""
LINK_URL_5 = ""
# =========================================================
# ユーティリティ関数
# =========================================================
def get_or_create_collection():
coll_name = f"{PREFIX_VAL}Collection"
coll = bpy.data.collections.get(coll_name)
if not coll:
coll = bpy.data.collections.new(coll_name)
bpy.context.scene.collection.children.link(coll)
return coll
def get_or_create_material(name, color, alpha):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = alpha
return mat
def create_or_update_mesh(obj_name, vertices, faces, mat):
coll = get_or_create_collection()
full_name = f"{PREFIX_VAL}{obj_name}"
obj = bpy.data.objects.get(full_name)
if not obj:
mesh = bpy.data.meshes.new(name=full_name)
obj = bpy.data.objects.new(full_name, mesh)
coll.objects.link(obj)
else:
mesh = obj.data
mesh.clear_geometry()
bm = bmesh.new()
for v in vertices:
bm.verts.new(v)
bm.verts.ensure_lookup_table()
if faces:
for f in faces:
bm.faces.new([bm.verts[i] for i in f])
elif len(vertices) == 2:
# 面がない場合(線分や矢印)、エッジを生成する処理を追加
bm.edges.new((bm.verts[0], bm.verts[1]))
bm.to_mesh(mesh)
bm.free()
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
return obj
def create_or_update_text(obj_name, text_body, loc, mat, size):
coll = get_or_create_collection()
full_name = f"{PREFIX_VAL}Text_{obj_name}"
obj = bpy.data.objects.get(full_name)
if not obj:
curve = bpy.data.curves.new(type="FONT", name=full_name)
obj = bpy.data.objects.new(full_name, curve)
coll.objects.link(obj)
obj.data.body = text_body
obj.location = loc
obj.data.size = size
obj.data.align_x = 'CENTER'
obj.data.align_y = 'CENTER'
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
# カメラの方を向かせる
obj.rotation_euler = (1.5708, 0, 0)
return obj
def create_arrow(obj_name, start_loc, direction, length, mat):
# 矢印は単純な線分で表現
end_loc = mathutils.Vector(start_loc) + (mathutils.Vector(direction).normalized() * length)
obj = create_or_update_mesh(obj_name, [start_loc, end_loc], [], mat)
obj.data.materials[0] = mat
return obj
# =========================================================
# リアルタイム更新ロジック
# =========================================================
def update_geometry(self, context):
props = context.scene.obs_props
if not props.is_active:
return
# 定数と変数
gh = props.green_half_length
ext = props.triangle_extension
alpha = props.alpha_val
text_size = props.text_size
# 座標定義
v_green_a = mathutils.Vector((-gh, 0, 0))
v_green_b = mathutils.Vector((gh, 0, 0))
v_blue_eye = mathutils.Vector((props.blue_eye_x, props.blue_eye_y, props.blue_eye_z))
v_red_eye = mathutils.Vector((props.red_eye_x, props.red_eye_y, props.red_eye_z))
# Y=0で止まらずに延長する(延長先を大きな底辺とする)
# ext=2.0のとき、Y=-10からY=10へ到達します
v_blue_ext_a = v_blue_eye + (v_green_a - v_blue_eye) * ext
v_blue_ext_b = v_blue_eye + (v_green_b - v_blue_eye) * ext
v_red_ext_a = v_red_eye + (v_green_a - v_red_eye) * ext
v_red_ext_b = v_red_eye + (v_green_b - v_red_eye) * ext
# マテリアル
mat_green = get_or_create_material(f"{PREFIX_VAL}Mat_Green", (0.0, 1.0, 0.5, 1.0), alpha)
mat_blue = get_or_create_material(f"{PREFIX_VAL}Mat_Blue", (0.0, 0.5, 1.0, 1.0), alpha)
mat_red = get_or_create_material(f"{PREFIX_VAL}Mat_Red", (1.0, 0.2, 0.2, 1.0), alpha)
mat_text = get_or_create_material(f"{PREFIX_VAL}Mat_Text", (1.0, 1.0, 1.0, 1.0), 1.0)
# 1. 共通事象の線分 (Green Edge)
create_or_update_mesh("GreenEdge", [v_green_a, v_green_b], [], mat_green)
# 2. 三角形ポリゴン (Observation Planes)
# y=0の線を越えて大きく広がる三角形を描画
obj_blue = create_or_update_mesh("BlueTriangle", [v_blue_eye, v_blue_ext_a, v_blue_ext_b], [[0, 1, 2]], mat_blue)
obj_red = create_or_update_mesh("RedTriangle", [v_red_eye, v_red_ext_a, v_red_ext_b], [[0, 1, 2]], mat_red)
# カスタムプロパティ(将来拡張用)
obj_blue["PastValue_Target"] = "Blue (Y>0)"
obj_red["PastValue_Target"] = "Red (Y<0)"
# 3. 過去方向の矢印 (Past Arrows)
if props.show_past_arrow:
x_axis = mathutils.Vector((1, 0, 0))
# 青の過去方向
blue_normal = mathutils.geometry.normal([v_blue_eye, v_blue_ext_b, v_blue_ext_a])
blue_past_dir = x_axis.cross(blue_normal)
if blue_past_dir.y < 0: # Y>0に向かうように反転
blue_past_dir = -blue_past_dir
create_arrow("BluePastArrow", (0,0,0), blue_past_dir, props.past_arrow_length, mat_blue)
# 赤の過去方向
red_normal = mathutils.geometry.normal([v_red_eye, v_red_ext_b, v_red_ext_a])
red_past_dir = x_axis.cross(red_normal)
if red_past_dir.y > 0: # Y<0に向かうように反転
red_past_dir = -red_past_dir
create_arrow("RedPastArrow", (0,0,0), red_past_dir, props.past_arrow_length, mat_red)
else:
# 非表示時はメッシュをクリア
create_or_update_mesh("BluePastArrow", [], [], mat_blue)
create_or_update_mesh("RedPastArrow", [], [], mat_red)
# 4. ラベル表示
if props.show_labels:
create_or_update_text("Label_Blue", "「俺」の観測平面 (青)\nY > 0 が過去", v_blue_eye + mathutils.Vector((0, -2, 0)), mat_text, text_size)
create_or_update_text("Label_Red", "「貴殿」の観測平面 (赤)\nY < 0 が過去", v_red_eye + mathutils.Vector((0, 2, 0)), mat_text, text_size)
create_or_update_text("Label_Green", "共通事象 (現在)", v_green_b + mathutils.Vector((1.5, 0, 0)), mat_text, text_size)
else:
create_or_update_text("Label_Blue", "", (0,0,0), mat_text, text_size)
create_or_update_text("Label_Red", "", (0,0,0), mat_text, text_size)
create_or_update_text("Label_Green", "", (0,0,0), mat_text, text_size)
# =========================================================
# プロパティグループ
# =========================================================
class OBS_Properties(bpy.types.PropertyGroup):
is_active: bpy.props.BoolProperty(default=False)
# 幾何学パラメータ
green_half_length: bpy.props.FloatProperty(name="Green Half Length", default=1.0, update=update_geometry)
triangle_extension: bpy.props.FloatProperty(name="Triangle Extension (延長率)", default=2.0, min=1.0, max=10.0, description="1.0でY=0まで、2.0で対向する目のY座標まで大きく広がります", update=update_geometry)
blue_eye_x: bpy.props.FloatProperty(name="Blue X", default=0.0, update=update_geometry)
blue_eye_y: bpy.props.FloatProperty(name="Blue Y", default=-10.0, update=update_geometry)
blue_eye_z: bpy.props.FloatProperty(name="Blue Z", default=-10.0, update=update_geometry)
red_eye_x: bpy.props.FloatProperty(name="Red X", default=0.0, update=update_geometry)
red_eye_y: bpy.props.FloatProperty(name="Red Y", default=10.0, update=update_geometry)
red_eye_z: bpy.props.FloatProperty(name="Red Z", default=-10.0, update=update_geometry)
# 表示オプション
show_past_arrow: bpy.props.BoolProperty(name="Show Past Arrow", default=True, update=update_geometry)
past_arrow_length: bpy.props.FloatProperty(name="Past Arrow Length", default=5.0, update=update_geometry)
show_labels: bpy.props.BoolProperty(name="Show Labels", default=True, update=update_geometry)
# 全体調整
alpha_val: bpy.props.FloatProperty(name="Opacity (透明度)", default=0.5, min=0.0, max=1.0, update=update_geometry)
text_size: bpy.props.FloatProperty(name="Text Size (文字サイズ)", default=1.0, min=0.1, max=10.0, update=update_geometry)
# =========================================================
# オペレーター (生成・切り離し)
# =========================================================
class OBS_OT_Generate(bpy.types.Operator):
bl_idname = "obs.generate"
bl_label = "観測平面を生成"
def execute(self, context):
context.scene.obs_props.is_active = True
update_geometry(self, context)
return {'FINISHED'}
class OBS_OT_Detach(bpy.types.Operator):
bl_idname = "obs.detach"
bl_label = "アドオンから切り離し"
bl_description = "現在の状態を独立したオブジェクトとして定着させます"
def execute(self, context):
context.scene.obs_props.is_active = False
coll_name = f"{PREFIX_VAL}Collection"
coll = bpy.data.collections.get(coll_name)
if coll:
coll.name = "Detached_ObservationPlanes"
for obj in coll.objects:
obj.name = obj.name.replace(PREFIX_VAL, "Detached_")
self.report({'INFO'}, "アドオンから切り離し、独立させました。")
return {'FINISHED'}
class OBS_OT_Uninstall(bpy.types.Operator):
bl_idname = "obs.uninstall"
bl_label = "アドオンを完全削除 (Unregister)"
bl_description = "このアドオンをシステムから解除し、UIから削除します"
def execute(self, context):
import sys
mod_name = __name__
if mod_name in sys.modules:
unregister()
self.report({'INFO'}, f"【{ADDON_TITLE}】をUnregisterしました。")
return {'FINISHED'}
# =========================================================
# UIパネル
# =========================================================
class OBS_PT_MainPanel(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = "OBS_PT_MainPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = context.scene.obs_props
layout.operator("obs.generate", icon='PLAY')
box = layout.box()
box.label(text="【リアルタイム調整】")
box.prop(props, "triangle_extension")
box.prop(props, "alpha_val")
box.prop(props, "text_size")
box = layout.box()
box.label(text="【表示切替】")
box.prop(props, "show_past_arrow")
box.prop(props, "show_labels")
box = layout.box()
box.label(text="【座標位置】")
col = box.column(align=True)
col.prop(props, "blue_eye_y")
col.prop(props, "blue_eye_z")
col.separator()
col.prop(props, "red_eye_y")
col.prop(props, "red_eye_z")
layout.separator()
layout.operator("obs.detach", icon='UNLINKED')
class OBS_PT_LinksPanel(bpy.types.Panel):
bl_label = "関連リンク集"
bl_idname = "OBS_PT_LinksPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_parent_id = "OBS_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
links = [
(LINK_NAME_1, LINK_URL_1),
(LINK_NAME_2, LINK_URL_2),
(LINK_NAME_3, LINK_URL_3),
(LINK_NAME_4, LINK_URL_4),
(LINK_NAME_5, LINK_URL_5)
]
for name, url in links:
if name and url:
layout.operator("wm.url_open", text=name, icon='URL').url = url
class OBS_PT_UninstallPanel(bpy.types.Panel):
bl_label = "アドオン削除"
bl_idname = "OBS_PT_UninstallPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_parent_id = "OBS_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.label(text="Unregisterを呼び出して解除します", icon='ERROR')
layout.operator("obs.uninstall", icon='TRASH')
# =========================================================
# 登録・解除
# =========================================================
classes = (
OBS_Properties,
OBS_OT_Generate,
OBS_OT_Detach,
OBS_OT_Uninstall,
OBS_PT_MainPanel,
OBS_PT_LinksPanel,
OBS_PT_UninstallPanel
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.obs_props = bpy.props.PointerProperty(type=OBS_Properties)
def unregister():
for cls in reversed(classes):
if hasattr(bpy.types, cls.__name__):
bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, "obs_props"):
del bpy.types.Scene.obs_props
if __name__ == "__main__":
register()