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