20261018 線路レール

rapture_20261018112724.png

小数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)を維持し、
     アドオンの無効化や再読み込みが何度行われても安全に終了できるようにすること。
"""