blender まとめ20260821

# =============================================================================
# Blender 5.x ONLY
# Generic Addon Base - Stable Core Template (Ver 1.8)
# =============================================================================

import bpy
import webbrowser
import re

# =============================================================================
# 01. 【USER CONFIG】 接頭辞・名称・リンク設定
# =============================================================================

# 1. 内部ID用:必ず「英文字」から開始し、英数字とアンダースコア(_)のみを使用してください。
ADDON_PREFIX = "CAM_SPHERE"

# 2. アドオンの表示名(Preferences / アドオン一覧に表示される名前)
PREF_DISPLAY_NAME = "Cam Sphere Links Master"

# 3. サイドバーのタブ名(3Dビュー Nパネルのタブ名)
SIDEBAR_TAB_NAME = "Cam Sphere"

# 4. リンク定義(名前, URL)
LINKS = (
    ("背後霊カメラ 最新版リンク", "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"),
    ("posfie", "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"),
    ("zionadchat", "<https://x.com/zionadchat>"),
)

# 5. 動作スイッチ
AUTO_OPEN_N_PANEL = True
AUTO_MATERIAL_PREVIEW = True

# =============================================================================
# 02. 【STABLE CORE LOGIC】 内部IDの自動生成と検証
# =============================================================================

_unregistered_flag = False

if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", ADDON_PREFIX):
    raise ValueError(
        f"[{PREF_DISPLAY_NAME}] ADDON_PREFIX は「英文字」から開始し、"
        f"英数字とアンダースコアのみ使用してください。現在の値: '{ADDON_PREFIX}'"
    )

_ID_VAL = ADDON_PREFIX.upper()
_ID_LOW = ADDON_PREFIX.lower()

OP_ID_URL = f"wm.{_ID_LOW}_open_url"
OP_ID_REMOVE = f"wm.{_ID_LOW}_remove_addon"
OP_ID_FOCUS = f"view3d.{_ID_LOW}_focus_selected"
PN_ID_MAIN = f"{_ID_VAL}_PT_main"
PN_ID_ADMIN = f"{_ID_VAL}_PT_admin"

# =============================================================================
# 03. アドオン情報 (bl_info)
# =============================================================================

bl_info = {
    "name": PREF_DISPLAY_NAME,
    "author": "Your Name",
    "version": (1, 8),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "フォーカス機能・リンク集・高安定タイマーを搭載したテンプレート",
    "category": "3D View",
}

# =============================================================================
# 04. Operators
# =============================================================================

class OT_focus_selected(bpy.types.Operator):
    """選択オブジェクトを画面中央に表示(フォーカス)"""
    bl_idname = OP_ID_FOCUS
    bl_label = "選択オブジェクトを中央に表示"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        if context.active_object or context.selected_objects:
            bpy.ops.view3d.view_selected()
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "オブジェクトが選択されていません。")
            return {'CANCELLED'}

class OT_open_url(bpy.types.Operator):
    """URLを検証してブラウザで開く"""
    bl_idname = OP_ID_URL
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()

    def execute(self, context):
        u = self.url.strip()
        if not u or not u.startswith(("http://", "https://")):
            self.report({'WARNING'}, f"有効なURLではありません: {u}")
            return {'CANCELLED'}
        webbrowser.open(u)
        return {'FINISHED'}

class OT_remove_addon(bpy.types.Operator):
    """安全な遅延解除タイマーを予約する"""
    bl_idname = OP_ID_REMOVE
    bl_label = "アドオン登録解除"

    def execute(self, context):
        if not bpy.app.timers.is_registered(delayed_unregister_timer):
            bpy.app.timers.register(delayed_unregister_timer, first_interval=0.1)
        return {'FINISHED'}

# =============================================================================
# 05. Panels
# =============================================================================

class PT_main(bpy.types.Panel):
    """メイン機能パネル:フォーカスボタンとリンク集"""
    bl_label = "Main Tools & Links"
    bl_idname = PN_ID_MAIN
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = SIDEBAR_TAB_NAME

    def draw(self, context):
        layout = self.layout
        
        # フォーカスボタンを最上部に配置
        col = layout.column(align=True)
        col.scale_y = 1.2
        col.operator(OP_ID_FOCUS, icon='ZOOM_SELECTED')
        
        layout.separator()
        
        # リンク集
        box = layout.box()
        box.label(text="Links", icon='URL')
        for name, url in LINKS:
            if name and url:
                box.operator(OP_ID_URL, text=name, icon='EXTERNAL_DRIVE').url = url

class PT_admin(bpy.types.Panel):
    """アドオン管理パネル"""
    bl_label = "Admin"
    bl_idname = PN_ID_ADMIN
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = SIDEBAR_TAB_NAME

    def draw(self, context):
        layout = self.layout
        layout.operator(OP_ID_REMOVE, icon='CANCEL')

# =============================================================================
# 06. Timer Logic
# =============================================================================

def initialize_viewport():
    """初期化:Nパネル展開とシェーディング設定"""
    wm = bpy.context.window_manager
    if wm is None: return 0.2
    for window in wm.windows:
        screen = window.screen
        if screen is None: continue
        for area in screen.areas:
            if area.type != 'VIEW_3D': continue
            space = area.spaces.active
            if space is None: continue
            if AUTO_OPEN_N_PANEL: space.show_region_ui = True
            if AUTO_MATERIAL_PREVIEW: space.shading.type = 'MATERIAL'
    return None

def delayed_unregister_timer():
    """Operator終了後に安全に解除を実行"""
    global _unregistered_flag
    if _unregistered_flag:
        return None
    unregister()
    return None

# =============================================================================
# 07. Register / Unregister
# =============================================================================

classes = (OT_focus_selected, OT_open_url, OT_remove_addon, PT_main, PT_admin)

def register():
    global _unregistered_flag
    _unregistered_flag = False
    
    for cls in classes:
        bpy.utils.register_class(cls)
        
    if not bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.register(initialize_viewport, first_interval=0.2)

def unregister():
    global _unregistered_flag
    if _unregistered_flag:
        return
    _unregistered_flag = True

    if bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.unregister(initialize_viewport)
    
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass

if __name__ == "__main__":
    try: unregister()
    except RuntimeError: pass
    register()

# =============================================================================
# PROMPT LOG:
# アドオンの有効化と同時に以下の機能を自動実行するBlender 5.x専用コードを作成。
# 1. 「サイドパネル(Nパネル)が自動で開く」
# 2. 「3Dビューがマテリアルプレビューに自動で切り替わる」
# 3. アドオン最上部に「選択オブジェクトを画面中央に表示(フォーカス)」するボタンを配置。
# 元の「リンクパネル(LINKS)」の構成を維持し、安定性の高いタイマー処理を組み込み。
# =============================================================================





# =============================================================================
# Blender 5.x ONLY
# Generic Addon Base - Stable Core Template (Ver 1.8)
# =============================================================================

import bpy
import webbrowser
import re

# =============================================================================
# 01. 【USER CONFIG】 接頭辞・名称・リンク設定
# =============================================================================

# 1. 内部ID用:必ず「英文字」から開始し、英数字とアンダースコア(_)のみを使用してください。
ADDON_PREFIX = "CAM_SPHERE"

# 2. アドオンの表示名(Preferences / アドオン一覧に表示される名前)
PREF_DISPLAY_NAME = "Cam Sphere Links Master"

# 3. サイドバーのタブ名(3Dビュー Nパネルのタブ名)
SIDEBAR_TAB_NAME = "Cam Sphere"

# 4. リンク定義(名前, URL)
LINKS = (
    ("背後霊カメラ 最新版リンク", "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"),
    ("posfie", "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"),
    ("zionadchat", "<https://x.com/zionadchat>"),
)

# 5. 動作スイッチ
AUTO_OPEN_N_PANEL = True
AUTO_MATERIAL_PREVIEW = True

# =============================================================================
# 02. 【STABLE CORE LOGIC】 内部IDの自動生成と検証
# =============================================================================

_unregistered_flag = False

if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", ADDON_PREFIX):
    raise ValueError(
        f"[{PREF_DISPLAY_NAME}] ADDON_PREFIX は「英文字」から開始し、"
        f"英数字とアンダースコアのみ使用してください。現在の値: '{ADDON_PREFIX}'"
    )

_ID_VAL = ADDON_PREFIX.upper()
_ID_LOW = ADDON_PREFIX.lower()

OP_ID_URL = f"wm.{_ID_LOW}_open_url"
OP_ID_REMOVE = f"wm.{_ID_LOW}_remove_addon"
OP_ID_FOCUS = f"view3d.{_ID_LOW}_focus_selected"
PN_ID_MAIN = f"{_ID_VAL}_PT_main"
PN_ID_ADMIN = f"{_ID_VAL}_PT_admin"

# =============================================================================
# 03. アドオン情報 (bl_info)
# =============================================================================

bl_info = {
    "name": PREF_DISPLAY_NAME,
    "author": "Your Name",
    "version": (1, 8),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "フォーカス機能・リンク集・高安定タイマーを搭載したテンプレート",
    "category": "3D View",
}

# =============================================================================
# 04. Operators
# =============================================================================

class OT_focus_selected(bpy.types.Operator):
    """選択オブジェクトを画面中央に表示(フォーカス)"""
    bl_idname = OP_ID_FOCUS
    bl_label = "選択オブジェクトを中央に表示"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        if context.active_object or context.selected_objects:
            bpy.ops.view3d.view_selected()
            return {'FINISHED'}
        else:
            self.report({'WARNING'}, "オブジェクトが選択されていません。")
            return {'CANCELLED'}

class OT_open_url(bpy.types.Operator):
    """URLを検証してブラウザで開く"""
    bl_idname = OP_ID_URL
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()

    def execute(self, context):
        u = self.url.strip()
        if not u or not u.startswith(("http://", "https://")):
            self.report({'WARNING'}, f"有効なURLではありません: {u}")
            return {'CANCELLED'}
        webbrowser.open(u)
        return {'FINISHED'}

class OT_remove_addon(bpy.types.Operator):
    """安全な遅延解除タイマーを予約する"""
    bl_idname = OP_ID_REMOVE
    bl_label = "アドオン登録解除"

    def execute(self, context):
        if not bpy.app.timers.is_registered(delayed_unregister_timer):
            bpy.app.timers.register(delayed_unregister_timer, first_interval=0.1)
        return {'FINISHED'}

# =============================================================================
# 05. Panels
# =============================================================================

class PT_main(bpy.types.Panel):
    """メイン機能パネル:フォーカスボタンとリンク集"""
    bl_label = "Main Tools & Links"
    bl_idname = PN_ID_MAIN
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = SIDEBAR_TAB_NAME

    def draw(self, context):
        layout = self.layout
        
        # フォーカスボタンを最上部に配置
        col = layout.column(align=True)
        col.scale_y = 1.2
        col.operator(OP_ID_FOCUS, icon='ZOOM_SELECTED')
        
        layout.separator()
        
        # リンク集
        box = layout.box()
        box.label(text="Links", icon='URL')
        for name, url in LINKS:
            if name and url:
                box.operator(OP_ID_URL, text=name, icon='EXTERNAL_DRIVE').url = url

class PT_admin(bpy.types.Panel):
    """アドオン管理パネル"""
    bl_label = "Admin"
    bl_idname = PN_ID_ADMIN
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = SIDEBAR_TAB_NAME

    def draw(self, context):
        layout = self.layout
        layout.operator(OP_ID_REMOVE, icon='CANCEL')

# =============================================================================
# 06. Timer Logic
# =============================================================================

def initialize_viewport():
    """初期化:Nパネル展開とシェーディング設定"""
    wm = bpy.context.window_manager
    if wm is None: return 0.2
    for window in wm.windows:
        screen = window.screen
        if screen is None: continue
        for area in screen.areas:
            if area.type != 'VIEW_3D': continue
            space = area.spaces.active
            if space is None: continue
            if AUTO_OPEN_N_PANEL: space.show_region_ui = True
            if AUTO_MATERIAL_PREVIEW: space.shading.type = 'MATERIAL'
    return None

def delayed_unregister_timer():
    """Operator終了後に安全に解除を実行"""
    global _unregistered_flag
    if _unregistered_flag:
        return None
    unregister()
    return None

# =============================================================================
# 07. Register / Unregister
# =============================================================================

classes = (OT_focus_selected, OT_open_url, OT_remove_addon, PT_main, PT_admin)

def register():
    global _unregistered_flag
    _unregistered_flag = False
    
    for cls in classes:
        bpy.utils.register_class(cls)
        
    if not bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.register(initialize_viewport, first_interval=0.2)

def unregister():
    global _unregistered_flag
    if _unregistered_flag:
        return
    _unregistered_flag = True

    if bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.unregister(initialize_viewport)
    
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass

if __name__ == "__main__":
    try: unregister()
    except RuntimeError: pass
    register()

# =============================================================================
# PROMPT LOG:
# アドオンの有効化と同時に以下の機能を自動実行するBlender 5.x専用コードを作成。
# 1. 「サイドパネル(Nパネル)が自動で開く」
# 2. 「3Dビューがマテリアルプレビューに自動で切り替わる」
# 3. アドオン最上部に「選択オブジェクトを画面中央に表示(フォーカス)」するボタンを配置。
# 元の「リンクパネル(LINKS)」の構成を維持し、安定性の高いタイマー処理を組み込み。
# =============================================================================
# =============================================================================
# Blender 5.x ONLY
# Generic Addon Base - Ultimate Stability Template (Ver 1.6)
# =============================================================================

import bpy
import webbrowser
import re

# =============================================================================
# 01. 【ここだけ変更】接頭辞・名称設定
# =============================================================================

# 内部ID用:必ず英数字とアンダースコア(_)のみを使用してください。
ADDON_PREFIX = "CAM_SPHERE"

# 1. アドオンの表示名(設定画面 / Preferences の一覧に表示される名前)
PREF_DISPLAY_NAME = "Cam Sphere Links Master"

# 2. サイドバーのタブ名(3DビューのNパネルに表示される名前)
SIDEBAR_TAB_NAME = "Cam Sphere"

# --- 厳格なPREFIX検証 (不正な文字がある場合はロード時にエラーを出して停止) ---
if not re.fullmatch(r"[A-Za-z0-9_]+", ADDON_PREFIX):
    raise ValueError(
        f"[{PREF_DISPLAY_NAME}] ADDON_PREFIX は英数字とアンダースコアのみ使用してください。 "
        f"現在の値: '{ADDON_PREFIX}'"
    )

# =============================================================================
# 02. アドオン情報 (bl_info)
# =============================================================================

bl_info = {
    "name": PREF_DISPLAY_NAME,
    "author": "Your Name",
    "version": (1, 6),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "専用リンク集および管理ツール(究極安定性テンプレート)",
    "category": "3D View",
}

# =============================================================================
# 03. 内部IDの自動生成
# =============================================================================

_ID_VAL = ADDON_PREFIX.upper()
_ID_LOW = ADDON_PREFIX.lower()

# 内部識別子
OP_ID_URL = f"wm.{_ID_LOW}_open_url"
OP_ID_REMOVE = f"wm.{_ID_LOW}_remove_addon"
PN_ID_LINKS = f"{_ID_VAL}_PT_links"
PN_ID_ADMIN = f"{_ID_VAL}_PT_admin"

# =============================================================================
# 04. Links定義 & 動作スイッチ
# =============================================================================

LINKS = (
    ("背後霊カメラ 最新版リンク", "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"),
    ("posfie", "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"),
    ("zionadchat", "<https://x.com/zionadchat>"),
)

AUTO_OPEN_N_PANEL = True
AUTO_MATERIAL_PREVIEW = True

# =============================================================================
# 05. Operators
# =============================================================================

class OT_open_url(bpy.types.Operator):
    """URLを検証してブラウザで開く"""
    bl_idname = OP_ID_URL
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()

    def execute(self, context):
        u = self.url.strip()
        if not u or not u.startswith(("http://", "https://")):
            self.report({'WARNING'}, f"有効なURLではありません: {u}")
            return {'CANCELLED'}
        webbrowser.open(u)
        return {'FINISHED'}

class OT_remove_addon(bpy.types.Operator):
    """安全な遅延解除タイマーを予約する"""
    bl_idname = OP_ID_REMOVE
    bl_label = "アドオン登録解除"

    def execute(self, context):
        if not bpy.app.timers.is_registered(delayed_unregister_timer):
            bpy.app.timers.register(delayed_unregister_timer, first_interval=0.1)
        return {'FINISHED'}

# =============================================================================
# 06. Panels
# =============================================================================

class PT_links(bpy.types.Panel):
    bl_label = "Links"
    bl_idname = PN_ID_LINKS
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = SIDEBAR_TAB_NAME  # 正しくタブ名に紐付け

    def draw(self, context):
        layout = self.layout
        for name, url in LINKS:
            if name and url:
                layout.operator(OP_ID_URL, text=name, icon='URL').url = url

class PT_admin(bpy.types.Panel):
    bl_label = "Admin"
    bl_idname = PN_ID_ADMIN
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = SIDEBAR_TAB_NAME  # 正しくタブ名に紐付け

    def draw(self, context):
        layout = self.layout
        layout.operator(OP_ID_REMOVE, icon='CANCEL')

# =============================================================================
# 07. Timer Logic
# =============================================================================

def initialize_viewport():
    wm = bpy.context.window_manager
    if wm is None: return 0.2
    for window in wm.windows:
        screen = window.screen
        if screen is None: continue
        for area in screen.areas:
            if area.type != 'VIEW_3D': continue
            space = area.spaces.active
            if space is None: continue
            if AUTO_OPEN_N_PANEL: space.show_region_ui = True
            if AUTO_MATERIAL_PREVIEW: space.shading.type = 'MATERIAL'
    return None

def delayed_unregister_timer():
    unregister()
    return None

# =============================================================================
# 08. Register / Unregister
# =============================================================================

classes = (OT_open_url, OT_remove_addon, PT_links, PT_admin)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    if not bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.register(initialize_viewport, first_interval=0.2)

def unregister():
    if bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.unregister(initialize_viewport)
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except RuntimeError: pass

if __name__ == "__main__":
    try: unregister()
    except RuntimeError: pass
    register()

# =============================================================================
# 【他AI向け HTML化 完全指示プロンプト】
#
# 1. 厳格なカラー設定(CSS変数):
#    --blog-background-color: #060505;   /* ページ背景(黒) */
#    --article-background-color: #0c120d; /* 記事背景(深緑) */
#    --article-text-color: #ffffff;       /* 本文文字(白) */
#    --heading-text-color: #ffffff;       /* 見出し文字(白) */
#    --quote-bar-color: #ffd1d1;          /* 引用バー(パステル赤) */
#    --quote-text-color: #cdf4d3;         /* 引用文字(パステル緑) */
#
# 2. 機能要件:
#    - PDF資料の内容を勝手に要約・改変せず、すべて一言一句そのまま出力せよ。
#    - 先頭に「読みやすさ重視(改行多め)」と「資料重視(そのまま)」の切替ボタンを設置せよ。
#    - 全ての句点(。)の後に <span class="period-break"></span> を挿入せよ。
# =============================================================================
# =============================================================================
# Blender 5.x ONLY
# Generic Addon Base - Ultimate Stability Template (Ver 1.5)
# =============================================================================

import bpy
import webbrowser
import re

# =============================================================================
# 01. 【ここだけ変更】接頭辞・名称設定
# =============================================================================

# 接頭辞:英数字とアンダースコアのみ使用可能です。
# 他のアドオンと競合しない一意な名前を付けてください(例: "MY_TOOLS_V1")
ADDON_PREFIX = "CAM_SPHERE"

# アドオンの表示名
ADDON_TITLE = "Cam Sphere"

# Nパネルのタブ名
ADDON_CATEGORY = "Cam Sphere"

# --- 厳格なPREFIX検証 (不正な文字がある場合はロード時にエラーを出す) ---
if not re.fullmatch(r"[A-Za-z0-9_]+", ADDON_PREFIX):
    raise ValueError(
        f"[{ADDON_TITLE}] ADDON_PREFIX は英数字とアンダースコアのみ使用してください。 "
        f"現在の値: '{ADDON_PREFIX}'"
    )

# =============================================================================
# 02. アドオン情報 (bl_info)
# =============================================================================

bl_info = {
    "name": f"{ADDON_TITLE} Links Master",
    "author": "Your Name",
    "version": (1, 5),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "専用リンク集および管理ツール(究極安定性テンプレート)",
    "category": "3D View",
}

# =============================================================================
# 03. 内部IDの自動生成 (これ以降のコードは原則変更不要)
# =============================================================================

_ID_VAL = ADDON_PREFIX.upper()
_ID_LOW = ADDON_PREFIX.lower()

# クラスで使用する共通ID
OP_ID_URL = f"wm.{_ID_LOW}_open_url"
OP_ID_REMOVE = f"wm.{_ID_LOW}_remove_addon"
PN_ID_LINKS = f"{_ID_VAL}_PT_links"
PN_ID_ADMIN = f"{_ID_VAL}_PT_admin"

# =============================================================================
# 04. Links定義 & 動作設定
# =============================================================================

LINKS = (
    ("背後霊カメラ 最新版リンク", "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"),
    ("posfie", "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"),
    ("zionadchat", "<https://x.com/zionadchat>"),
)

AUTO_OPEN_N_PANEL = True
AUTO_MATERIAL_PREVIEW = True

# =============================================================================
# 05. Operators
# =============================================================================

class OT_open_url(bpy.types.Operator):
    """URLを検証してブラウザで開く"""
    bl_idname = OP_ID_URL
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()

    def execute(self, context):
        u = self.url.strip()

        # URLの妥当性チェック
        if not u or not u.startswith(("http://", "https://")):
            self.report({'WARNING'}, f"有効なURLではありません: {u}")
            return {'CANCELLED'}

        webbrowser.open(u)
        return {'FINISHED'}

class OT_remove_addon(bpy.types.Operator):
    """安全な遅延解除タイマーを予約する"""
    bl_idname = OP_ID_REMOVE
    bl_label = "アドオン登録解除"
    bl_description = "現在ロードされているアドオンの登録を安全に解除します"

    def execute(self, context):
        # 二重登録を防止してタイマーを開始
        if not bpy.app.timers.is_registered(delayed_unregister_timer):
            bpy.app.timers.register(delayed_unregister_timer, first_interval=0.1)
        return {'FINISHED'}

# =============================================================================
# 06. Panels
# =============================================================================

class PT_links(bpy.types.Panel):
    bl_label = "Links"
    bl_idname = PN_ID_LINKS
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        for name, url in LINKS:
            if name and url:
                layout.operator(OP_ID_URL, text=name, icon='URL').url = url

class PT_admin(bpy.types.Panel):
    bl_label = "Admin"
    bl_idname = PN_ID_ADMIN
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        layout.operator(OP_ID_REMOVE, icon='CANCEL')

# =============================================================================
# 07. Timer Logic
# =============================================================================

def initialize_viewport():
    """初期化処理:Nパネル展開とマテリアルプレビュー設定"""
    wm = bpy.context.window_manager
    if wm is None: return 0.2
    
    for window in wm.windows:
        screen = window.screen
        if screen is None: continue
        for area in screen.areas:
            if area.type != 'VIEW_3D': continue
            space = area.spaces.active
            if space is None: continue
            
            if AUTO_OPEN_N_PANEL:
                space.show_region_ui = True
            if AUTO_MATERIAL_PREVIEW:
                space.shading.type = 'MATERIAL'
    return None

def delayed_unregister_timer():
    """Operator終了後に安全に解除を実行 (TimerはNoneを返すと自動解除される)"""
    unregister()
    return None

# =============================================================================
# 08. Register / Unregister
# =============================================================================

classes = (OT_open_url, OT_remove_addon, PT_links, PT_admin)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    
    if not bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.register(initialize_viewport, first_interval=0.2)

def unregister():
    # 初期化タイマーの解除
    if bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.unregister(initialize_viewport)

    # 【改善】delayed_unregister_timerの自己解除チェックを廃止
    # Timer自身がNoneを返して終了する設計に任せ、
    # ここではクラス登録解除のみを確実に行う

    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            # すでに解除されている場合は無視
            pass

if __name__ == "__main__":
    try:
        unregister()
    except RuntimeError:
        pass
    register()

# =============================================================================
# 【他AI向け HTML化 完全指示プロンプト】
#
# このアドオンをHTML化する場合は、以下の【要件】を完全厳守せよ。
#
# 1. 厳格なカラー設定(CSS変数):
#    --blog-background-color: #060505;   /* ページ背景(黒) */
#    --article-background-color: #0c120d; /* 記事背景(深緑) */
#    --article-text-color: #ffffff;       /* 本文文字(白) */
#    --heading-text-color: #ffffff;       /* 見出し文字(白) */
#    --quote-bar-color: #ffd1d1;          /* 引用バー(パステル赤) */
#    --quote-text-color: #cdf4d3;         /* 引用文字(パステル緑) */
#
# 2. 機能要件:
#    - PDF資料の内容を勝手に要約・改変せず、すべて一言一句そのまま出力せよ。
#    - 先頭に「読みやすさ重視(改行多め)」と「資料重視(そのまま)」の切替ボタンを設置せよ。
#    - 全ての句点(。)の後に <span class="period-break"></span> を挿入せよ。
# =============================================================================
# =============================================================================
# Blender 5.x ONLY
# Generic Addon Base - Prefix Configurable Template (Ver 1.4)
# =============================================================================

bl_info = {
    "name": "Cam Sphere Links Master",
    "author": "Your Name",
    "version": (1, 4),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "専用リンク集および管理ツール(先頭Prefix変更モデル)",
    "category": "3D View",
}

import bpy
import webbrowser

# =============================================================================
# 01. 【ここだけ変更】接頭辞設定
# =============================================================================

# この値を変更するだけで、内部の全てのIDが衝突しないように自動変更されます
# (英数字とアンダースコアのみ使用してください)
ADDON_PREFIX = "CAM_SPHERE"

# パネルに表示される名称
ADDON_TITLE = "Cam Sphere"
# Nパネルのタブ名
ADDON_CATEGORY = "Cam Sphere"

# =============================================================================
# 02. 内部IDの自動生成(これ以降のコードは変更不要です)
# =============================================================================

_ID_CLEAN = ADDON_PREFIX.strip().replace(" ", "_").replace("-", "_")
_ID_VAL = _ID_CLEAN.upper()
_ID_LOW = _ID_CLEAN.lower()

# クラスで使用する共通ID定義
OP_ID_URL = f"wm.{_ID_LOW}_open_url"
OP_ID_REMOVE = f"wm.{_ID_LOW}_remove_addon"
PN_ID_LINKS = f"{_ID_VAL}_PT_links"
PN_ID_ADMIN = f"{_ID_VAL}_PT_admin"

# =============================================================================
# 03. Links定義
# =============================================================================

LINKS = (
    ("背後霊カメラ 最新版リンク", "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"),
    ("posfie", "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"),
    ("zionadchat", "<https://x.com/zionadchat>"),
)

# =============================================================================
# 04. 動作スイッチ
# =============================================================================

AUTO_OPEN_N_PANEL = True
AUTO_MATERIAL_PREVIEW = True

# =============================================================================
# 05. Operators
# =============================================================================

class OT_open_url(bpy.types.Operator):
    bl_idname = OP_ID_URL
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()

    def execute(self, context):
        u = self.url.strip()
        if u and u.startswith(("http://", "https://")):
            webbrowser.open(u)
        return {'FINISHED'}

class OT_remove_addon(bpy.types.Operator):
    bl_idname = OP_ID_REMOVE
    bl_label = "アドオン登録解除"

    def execute(self, context):
        if not bpy.app.timers.is_registered(delayed_unregister_timer):
            bpy.app.timers.register(delayed_unregister_timer, first_interval=0.1)
        return {'FINISHED'}

# =============================================================================
# 06. Panels
# =============================================================================

class PT_links(bpy.types.Panel):
    bl_label = "Links"
    bl_idname = PN_ID_LINKS
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        for name, url in LINKS:
            if name and url:
                layout.operator(OP_ID_URL, text=name, icon='URL').url = url

class PT_admin(bpy.types.Panel):
    bl_label = "Admin"
    bl_idname = PN_ID_ADMIN
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        layout.operator(OP_ID_REMOVE, icon='CANCEL')

# =============================================================================
# 07. Timer Logic
# =============================================================================

def initialize_viewport():
    wm = bpy.context.window_manager
    if wm is None: return 0.2
    for window in wm.windows:
        screen = window.screen
        if screen is None: continue
        for area in screen.areas:
            if area.type != 'VIEW_3D': continue
            space = area.spaces.active
            if space is None: continue
            if AUTO_OPEN_N_PANEL: space.show_region_ui = True
            if AUTO_MATERIAL_PREVIEW: space.shading.type = 'MATERIAL'
    return None

def delayed_unregister_timer():
    unregister()
    return None

# =============================================================================
# 08. Register / Unregister
# =============================================================================

classes = (OT_open_url, OT_remove_addon, PT_links, PT_admin)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    if not bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.register(initialize_viewport, first_interval=0.2)

def unregister():
    if bpy.app.timers.is_registered(delayed_unregister_timer):
        bpy.app.timers.unregister(delayed_unregister_timer)
    if bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.unregister(initialize_viewport)
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except RuntimeError: pass

if __name__ == "__main__":
    try: unregister()
    except RuntimeError: pass
    register()

# =============================================================================
# 【他AI向け HTML化 完全指示プロンプト】
#
# このアドオンをHTML化する場合は、以下の【要件】を完全厳守せよ。
#
# 1. 厳格なカラー設定(CSS変数):
#    --blog-background-color: #060505;   /* ページ背景(黒) */
#    --article-background-color: #0c120d; /* 記事背景(深緑) */
#    --article-text-color: #ffffff;       /* 本文文字(白) */
#    --heading-text-color: #ffffff;       /* 見出し文字(白) */
#    --quote-bar-color: #ffd1d1;          /* 引用バー(パステル赤) */
#    --quote-text-color: #cdf4d3;         /* 引用文字(パステル緑) */
#
# 2. 機能要件:
#    - PDF資料の内容を勝手に要約・改変せず、すべて一言一句そのまま出力せよ。
#    - 先頭に「読みやすさ重視(改行多め)」と「資料重視(そのまま)」の切替ボタンを設置せよ。
#    - 全ての句点(。)の後に <span class="period-break"></span> を挿入せよ。
# =============================================================================
# =============================================================================
# Blender 5.x ONLY
# Generic Addon Base - Ultimate Stability Template (Ver 1.3)
# =============================================================================

bl_info = {
    "name": "Cam Sphere Links Master",
    "author": "Your Name",
    "version": (1, 3),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "専用リンク集および管理ツール(究極安定性テンプレート)",
    "category": "3D View",
}

import bpy
import webbrowser

# =============================================================================
# 01. 基本設定(定義セクション)
# =============================================================================

# 【ルール】PREFIXは必ず英数字とアンダースコア(_)のみを使用してください
PREFIX = "CAM_SPHERE"                  
ADDON_TITLE = "Cam Sphere"             
ADDON_CATEGORY = "Cam Sphere"          

# =============================================================================
# 02. IDの動的生成(内部識別子の一貫性確保)
# =============================================================================

# 安全のため、記号をアンダースコアに置換する処理を追加
PREFIX_CLEAN = PREFIX.strip().replace(" ", "_").replace("-", "_")
PREFIX_VAL = PREFIX_CLEAN.upper()
PREFIX_ID = PREFIX_CLEAN.lower()

# Operator / Panel IDs
BL_ID_OPEN_URL = f"wm.{PREFIX_ID}_open_url"
BL_ID_REMOVE_ADDON = f"wm.{PREFIX_ID}_remove_addon"
PANEL_ID_LINKS = f"{PREFIX_VAL}_PT_links"
PANEL_ID_ADMIN = f"{PREFIX_VAL}_PT_admin"

# =============================================================================
# 03. Links定義
# =============================================================================

LINKS = (
    ("背後霊カメラ 最新版リンク", "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"),
    ("posfie", "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"),
    ("zionadchat", "<https://x.com/zionadchat>"),
)

# =============================================================================
# 04. 動作スイッチ
# =============================================================================

AUTO_OPEN_N_PANEL = True
AUTO_MATERIAL_PREVIEW = True

# =============================================================================
# 05. Operators
# =============================================================================

class OT_open_url(bpy.types.Operator):
    """URLを検証してブラウザで開く"""
    bl_idname = BL_ID_OPEN_URL
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()

    def execute(self, context):
        url = self.url.strip()
        if not url or not url.startswith(("http://", "https://")):
            self.report({'WARNING'}, "無効なURLです。")
            return {'CANCELLED'}
        webbrowser.open(url)
        return {'FINISHED'}

class OT_remove_addon(bpy.types.Operator):
    """Timerを介して安全にアドオンを登録解除する"""
    bl_idname = BL_ID_REMOVE_ADDON
    bl_label = "アドオン登録解除"
    bl_description = "現在ロードされているアドオンの登録を安全に解除します"

    def execute(self, context):
        # 【改善1】解除Timerの多重登録を防止
        if not bpy.app.timers.is_registered(delayed_unregister_timer):
            bpy.app.timers.register(delayed_unregister_timer, first_interval=0.1)
        return {'FINISHED'}

# =============================================================================
# 06. Panels
# =============================================================================

class PT_links(bpy.types.Panel):
    bl_label = "Links"
    bl_idname = PANEL_ID_LINKS
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        for name, url in LINKS:
            if name and url:
                layout.operator(BL_ID_OPEN_URL, text=name, icon='URL').url = url

class PT_admin(bpy.types.Panel):
    bl_label = "Admin"
    bl_idname = PANEL_ID_ADMIN
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        layout.operator(BL_ID_REMOVE_ADDON, icon='CANCEL')

# =============================================================================
# 07. Timer Logic (初期化と遅延解除)
# =============================================================================

def initialize_viewport():
    """3D Viewの状態を初期化する(WindowManagerの準備が整うまで再試行)"""
    wm = bpy.context.window_manager
    if wm is None: return 0.2

    for window in wm.windows:
        screen = window.screen
        if screen is None: continue
        for area in screen.areas:
            if area.type != 'VIEW_3D': continue
            space = area.spaces.active
            if space is None: continue
            if AUTO_OPEN_N_PANEL: space.show_region_ui = True
            if AUTO_MATERIAL_PREVIEW: space.shading.type = 'MATERIAL'
    return None

def delayed_unregister_timer():
    """Operator終了後に安全に解除を実行する"""
    unregister()
    return None

# =============================================================================
# 08. Register / Unregister (究極安定版ロジック)
# =============================================================================

classes = (
    OT_open_url,
    OT_remove_addon,
    PT_links,
    PT_admin,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    
    if not bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.register(initialize_viewport, first_interval=0.2)

def unregister():
    # 【改善2】解除Timer自体の登録も、unregister時に確実にクリアする
    if bpy.app.timers.is_registered(delayed_unregister_timer):
        bpy.app.timers.unregister(delayed_unregister_timer)

    if bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.unregister(initialize_viewport)

    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass

if __name__ == "__main__":
    # 【改善3】例外処理をRuntimeErrorに限定し、信頼性を向上
    try:
        unregister()
    except RuntimeError:
        pass
    register()

# =============================================================================
# 【他AI向け HTML化・PDF内容厳守プロンプト】
#
# このアドオンの説明をHTML化する場合は、以下の【指示】を最優先せよ。
#
# 1. 配色(CSS変数):
#    --blog-background-color: #060505;
#    --article-background-color: #0c120d;
#    --article-text-color: #ffffff;
#    --heading-text-color: #ffffff;
#    --quote-bar-color: #ffd1d1;
#    --quote-text-color: #cdf4d3;
#
# 2. コンテンツ(PDF内容厳守):
#    提供された資料の内容を一切要約・省略・改変せず、すべてそのままHTML化せよ。
#    改行のON/OFF切り替えボタン(読みやすさ重視/資料重視)を必ず実装せよ。
#    文章内の全ての句点(。)の直後に <span class="period-break"></span> を挿入せよ。
# =============================================================================
# =============================================================================
# Blender 5.x ONLY
# Generic Addon Base - High Stability Template
# =============================================================================

bl_info = {
    "name": "Cam Sphere Links Master",
    "author": "Your Name",
    "version": (1, 2),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "専用リンク集および管理ツール(高安定性テンプレート)",
    "category": "3D View",
}

import bpy
import webbrowser

# =============================================================================
# 01. 基本設定(ここを変更するだけで独立した別アドオンになります)
# =============================================================================

PREFIX = "CAM_SPHERE"                  # 内部データ用の一意な接頭辞
ADDON_TITLE = "Cam Sphere"             # パネル等に表示される名前
ADDON_CATEGORY = "Cam Sphere"          # Nパネルのタブ名

# =============================================================================
# 02. IDの動的生成(Operator IDやPanel IDにのみ利用)
# =============================================================================

PREFIX_VAL = PREFIX.upper().strip().replace(" ", "_")
PREFIX_ID = PREFIX.lower().strip().replace(" ", "_")

# Operator IDs
BL_ID_OPEN_URL = f"wm.{PREFIX_ID}_open_url"
BL_ID_REMOVE_ADDON = f"wm.{PREFIX_ID}_remove_addon"

# Panel IDs
PANEL_ID_LINKS = f"{PREFIX_VAL}_PT_links"
PANEL_ID_ADMIN = f"{PREFIX_VAL}_PT_admin"

# =============================================================================
# 03. Links定義(一本化管理)
# =============================================================================

LINKS = (
    ("背後霊カメラ 最新版リンク", "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"),
    ("posfie", "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"),
    ("zionadchat", "<https://x.com/zionadchat>"),
    # ("名前", "URL"),  # 追加が必要な場合はここに追記
)

# =============================================================================
# 04. 動作スイッチ
# =============================================================================

AUTO_OPEN_N_PANEL = True
AUTO_MATERIAL_PREVIEW = True

# =============================================================================
# 05. Operators
# =============================================================================

class OT_open_url(bpy.types.Operator):
    """URLを検証してブラウザで開く"""
    bl_idname = BL_ID_OPEN_URL
    bl_label = "リンクを開く"
    
    url: bpy.props.StringProperty()

    def execute(self, context):
        url = self.url.strip()
        
        if not url or not url.startswith(("http://", "https://")):
            self.report({'WARNING'}, "有効なURLではありません。")
            return {'CANCELLED'}
            
        webbrowser.open(url)
        return {'FINISHED'}

class OT_remove_addon(bpy.types.Operator):
    """Timerを介して安全にアドオンを登録解除する"""
    bl_idname = BL_ID_REMOVE_ADDON
    bl_label = "アドオン登録解除"
    bl_description = "現在ロードされているアドオンの登録を安全に解除します"

    def execute(self, context):
        # 自分自身のOperator実行終了後にunregisterを呼び出すためTimerを使用
        bpy.app.timers.register(delayed_unregister_timer, first_interval=0.1)
        return {'FINISHED'}

# =============================================================================
# 06. Panels
# =============================================================================

class PT_links(bpy.types.Panel):
    bl_label = "Links"
    bl_idname = PANEL_ID_LINKS
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        for name, url in LINKS:
            if name and url:
                layout.operator(BL_ID_OPEN_URL, text=name, icon='URL').url = url

class PT_admin(bpy.types.Panel):
    bl_label = "Admin"
    bl_idname = PANEL_ID_ADMIN
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        # 名称を「登録解除」に変更して正確性を向上
        layout.operator(BL_ID_REMOVE_ADDON, text="アドオン登録解除", icon='CANCEL')

# =============================================================================
# 07. Timer Logic (初期化と遅延解除)
# =============================================================================

def initialize_viewport():
    """3D Viewの状態を初期化する(WindowManagerの準備が整うまで再試行)"""
    wm = bpy.context.window_manager
    if wm is None:
        return 0.2  # WindowManagerがない場合は0.2秒後に再試行

    for window in wm.windows:
        screen = window.screen
        if screen is None: continue
        
        for area in screen.areas:
            if area.type != 'VIEW_3D': continue
            
            space = area.spaces.active
            if space is None: continue

            if AUTO_OPEN_N_PANEL:
                space.show_region_ui = True
            
            if AUTO_MATERIAL_PREVIEW:
                space.shading.type = 'MATERIAL'
    
    return None  # 成功した場合はTimerを終了

def delayed_unregister_timer():
    """Operator終了後に安全に解除を実行する"""
    unregister()
    return None

# =============================================================================
# 08. Register / Unregister (安定版ロジック)
# =============================================================================

classes = (
    OT_open_url,
    OT_remove_addon,
    PT_links,
    PT_admin,
)

def register():
    # クラス名書き換えを廃止し、標準的な登録を行う
    for cls in classes:
        bpy.utils.register_class(cls)
    
    # 二重登録を防止してTimerを登録
    if not bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.register(initialize_viewport, first_interval=0.2)

def unregister():
    # 初期化Timerの登録解除
    if bpy.app.timers.is_registered(initialize_viewport):
        bpy.app.timers.unregister(initialize_viewport)

    # クラスの登録解除(逆順、想定されるRuntimeErrorのみ無視)
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except RuntimeError:
            pass

if __name__ == "__main__":
    # テキストエディタからの実行時は一度解除を試みてから登録
    try: unregister()
    except: pass
    register()

# =============================================================================
# 【他AI向け HTML化・PDF内容厳守プロンプト】
#
# このコードの説明をHTML化する場合は、以下の指示を最優先せよ。
#
# 1. 配色(CSS変数):
#    --blog-background-color: #060505;
#    --article-background-color: #0c120d;
#    --article-text-color: #ffffff;
#    --quote-bar-color: #ffd1d1;
#    --quote-text-color: #cdf4d3;
#
# 2. コンテンツ:
#    PDF資料の内容を一切要約・省略せず、すべてそのままHTML化せよ。
#    改行のON/OFF切り替えボタンを必ず実装し、句点(。)の後に
#    <span class="period-break"></span> を挿入せよ。
# =============================================================================