サイドバー", } # === DEFAULTS_BLOCK_START === CURRENT_DEFAULTS = { "sun_control_mode": 'ANGLE', "grid_preset": 'white', "wire_preset": 'orange', "camera_preset": 'Cam', "background_type": 'LIN"> サイドバー", } # === DEFAULTS_BLOCK_START === CURRENT_DEFAULTS = { "sun_control_mode": 'ANGLE', "grid_preset": 'white', "wire_preset": 'orange', "camera_preset": 'Cam', "background_type": 'LIN"> サイドバー", } # === DEFAULTS_BLOCK_START === CURRENT_DEFAULTS = { "sun_control_mode": 'ANGLE', "grid_preset": 'white', "wire_preset": 'orange', "camera_preset": 'Cam', "background_type": 'LIN">
import bpy
import math
import re
from bpy.props import FloatVectorProperty, FloatProperty, EnumProperty, BoolProperty, PointerProperty, StringProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector, Euler
from math import radians
from datetime import datetime

# ==============================================================================
# 【 開発方針 】
# 本アドオンは Blender 5.x 専用です。Blender 4.x以前との互換性は維持しません。
# ==============================================================================

# アドオンのメタデータ
bl_info = {
    "name": "zionad [ 500 背景色 ]",
    "author": "zionadchat",
    "version": (3, 11, 0),
    "blender": (5, 0, 0),
    "category": "[ 500 背景色 ]",
    "description": "【Blender 5.x 専用】3Dビューポートの色、太陽、透視投影視座位置をリアルタイム制御します",
    "location": "3Dビュー > サイドバー",
}

# === DEFAULTS_BLOCK_START ===
CURRENT_DEFAULTS = {
    "sun_control_mode": 'ANGLE',
    "grid_preset": 'white',
    "wire_preset": 'orange',
    "camera_preset": 'Cam',
    "background_type": 'LINEAR',
    "header_preset": 'Dark Green',
    "preset": 'Blue20260725',
    "render_preset": 'Blue',
    "outliner_preset": 'Outliner 5.0.0',
    "text_editor_preset": 'Text 5.0.0',
    "sun_target_location": (0.00, 0.00, 0.00),
    "sun_rotation": (0.79, 0.00, 0.79),
    "sun_location": (0.00, 0.00, 10.00),
    "sun_strength": 2.50,
    "custom_grid_scale": 1.00,
    "grid_color": (0.21, 0.21, 0.21, 1.00),
    "wire_color": (0.71, 0.21, 0.05),
    "camera_color": (0.47, 0.55, 1.00),
    "header_color": (0.00, 0.03, 0.00, 1.00),
    "custom_gradient_high": (0.02, 0.03, 0.07),
    "custom_gradient_low": (0.15, 0.15, 0.47),
    "reverse_gradient": False,
    "render_color": (0.19, 0.60, 1.00, 1.00),
    "render_environment_strength": 1.00,
    "outliner_header_color": (0.19, 0.19, 0.19, 0.70),
    "outliner_background_color": (0.14, 0.14, 0.14, 1.00),
    "text_editor_header_color": (0.19, 0.19, 0.19, 0.70),
    "text_editor_background_color": (0.14, 0.14, 0.14, 1.00),
    "view_pos": (4.16, -22.88, 6.64),
}
# === DEFAULTS_BLOCK_END ===

# ==============================================================================
# コアロジック
# ==============================================================================

# 定数
ADDON_CATEGORY_NAME = bl_info["category"]
PREFIX = "backcolor2026"

VIEW_RESET_BTN_TEXT = "Reset View (0, -10, 10)"  
VIEW_POS_INIT = (0.0, -10.0, 10.0)  

# パネル定義
LINK_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_link_panel"
MAIN_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_main_panel"
PERSP_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_persp_control"
BG_PANEL_IDNAME_1 = f"{PREFIX}_VIEW3D_PT_solid_background_panel"
HEADER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_header_panel"
RENDER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_render_panel"
SUN_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_sun_panel"
GRID_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_gridpanel"
WIRE_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_wirepanel"
CAMERA_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_camerapanel"
OUTLINER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_outliner_panel"
TEXT_EDITOR_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_text_editor_panel"
REMOVE_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_remove"

# パネルラベル
PANEL_LABELS = {
    "LINK": "リンク",
    "MAIN": "メイン操作",
    "PERSP": "透視投影 視座位置",
    "BACKGROUND": "3D Viewport Color",
    "HEADER": "Header Color",
    "RENDER": "Render Color",
    "SUN": "太陽 設定",
    "GRID": "Grid Color",
    "WIRE": "Wire Color",
    "CAMERA": "Camera Color",
    "OUTLINER": "Outliner Color",
    "TEXT_EDITOR": "Text Editor Color",
    "REMOVE": "アドオン削除",
}

# === PRESETS_BLOCK_START ===
BASE_PRESETS =[
    ("55551255", "Nlue GGG", "New custom background", (0.02, 0.02, 0.39), (0.01, 0.07, 0.01)),
    ("Dark Green", "Dark Green", "Dark Green background", (0.00, 0.28, 0.02), (0.10, 0.15, 0.05)),
    ("purple", "purple", "purple background", (0.49, 0.45, 1.00), (0.74, 0.65, 0.88)),
    ("BlueGreen", "BlueGreen", "BlueGreen background", (0.14, 0.34, 0.83), (0.57, 0.88, 0.63)),
    ("DARK_BLUE", "Dark Blue", "Dark Blue background", (0.07, 0.13, 0.31), (0.05, 0.05, 0.15)),
    ("Viewport 5.0.0", "Viewport 5.0.0", "Viewport 5.0.0 background", (0.24, 0.24, 0.24), (0.19, 0.19, 0.19)),
    ("FOREST_GREEN", "Forest Green", "Forest Green background", (0.50, 0.70, 0.50), (0.10, 0.15, 0.05)),
    ("725827828", "Soft Purple", "20260722", (0.14, 0.46, 1.00), (0.90, 0.80, 1.00)),
    ("Blue20260725", "Blue20260725", "New custom background", (0.02, 0.03, 0.07), (0.15, 0.15, 0.47)),
]
# === PRESETS_BLOCK_END ===

HEADER_PRESETS =[
    ("Dark Green", "Dark Green", "Dark Green header", (0.00, 0.03, 0.00, 1.00)),
    ("purple", "purple", "purple header", (0.00, 0.00, 0.00, 1.00)),
    ("BlueGreen", "BlueGreen", "BlueGreen header", (0.00, 0.00, 0.00, 1.00)),
    ("DARK_BLUE", "Dark Blue", "Dark Blue header", (0.10, 0.10, 0.30, 1.00)),
    ("Viewport 5.0.0", "Viewport 5.0.0", "Viewport 5.0.0 header", (0.19, 0.19, 0.19, 0.70)),
    ("FOREST_GREEN", "Forest Green", "Forest Green header", (0.20, 0.30, 0.10, 1.00)),
]
RENDER_PRESETS =[
    ("Blue", "Blue", "Blue render color", (0.19, 0.60, 1.00, 1.00)),
    ("Render 5.0.0", "Render 5.0.0", "Render 5.0.0 color", (0.05, 0.05, 0.05, 1.00)),
    ("LIGHT_GRAY", "Light Gray", "Light gray render", (0.80, 0.80, 0.80, 1.00)),
]
GRID_PRESETS =[
    ("white", "white", "white grid color", (1.00, 1.00, 1.00, 1.00)),
    ("Grid 5.0.0", "Grid 5.0.0", "Grid 5.0.0 color", (0.33, 0.33, 0.33, 0.50)),
    ("DARK_GRAY", "Dark Gray", "Dark gray grid", (0.10, 0.10, 0.10, 1.00)),
]
WIRE_PRESETS =[
    ("orange", "orange", "orange wire", (0.71, 0.21, 0.05)),
    ("Wire 5.0.0", "Wire 5.0.0", "Wire 5.0.0 color", (0.00, 0.00, 0.00)),
    ("WHITE", "White", "White wire", (1.00, 1.00, 1.00)),
]
CAMERA_PRESETS =[
    ("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
    ("Cam 5.0.0", "Cam 5.0.0", "Cam 5.0.0 color", (0.00, 0.00, 0.00)),
    ("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]
OUTLINER_PRESETS =[
    ("Outliner 5.0.0", "Outliner 5.0.0", "Outliner 5.0.0 colors", (0.19, 0.19, 0.19, 0.70), (0.14, 0.14, 0.14, 1.00)),
    ("DARK_TEAL", "Dark Teal", "Dark teal outliner", (0.00, 0.20, 0.20, 1.00), (0.00, 0.10, 0.10, 1.00)),
]
TEXT_EDITOR_PRESETS =[
    ("Text 5.0.0", "Text 5.0.0", "Text Editor 5.0.0 colors", (0.19, 0.19, 0.19, 0.70), (0.14, 0.14, 0.14, 1.00)),
    ("DARK_GREEN", "Dark Green", "Dark green text editor", (0.00, 0.20, 0.00, 1.00), (0.00, 0.10, 0.00, 1.00)),
]
BACKGROUND_TYPES =[
    ('SINGLE_COLOR', "Single Color", "Uniform background color"),
    ('LINEAR', "Linear", "Linear gradient background"),
    ('RADIAL', "Vignette", "Radial gradient simulating a vignette effect"),
]

def get_safe_default(value, presets):
    valid_ids = [p[0] for p in presets]
    return value if value in valid_ids else (valid_ids[0] if valid_ids else "")

# ==============================================================================
# 文字化け・誤爆回避 ユーティリティ関数
# ==============================================================================
def get_own_source_code():
    import sys, os
    try:
        filepath = sys.modules[__name__].__file__
        if os.path.exists(filepath):
            with open(filepath, 'r', encoding='utf-8') as f:
                return f.read()
    except Exception: pass
    for text in bpy.data.texts:
        content = text.as_string()
        if "bl_info = {" in content and "backcolor2026" in content and "get_own_source_code" in content:
            return content
    return ""

def generate_current_defaults_str(context):
    """ 文字列の分割(+結合)により、置換システム自体の誤爆を回避する """
    props = context.scene.viewport_color_props
    sun_props = context.scene.sun_settings_props
    persp_props = context.scene.persp_view_props

    def fmt_vec(v): return "(" + ", ".join(f"{x:.2f}" for x in v) + ")"
    def fmt_str(s): return f"'{s}'"

    lines = []
    lines.append("# ===" + " DEFAULTS_BLOCK_START ===")
    lines.append("CURRENT_DEFAULTS = {")
    lines.append(f'    "sun_control_mode": {fmt_str(sun_props.sun_control_mode)},')
    lines.append(f'    "grid_preset": {fmt_str(props.grid_preset)},')
    lines.append(f'    "wire_preset": {fmt_str(props.wire_preset)},')
    lines.append(f'    "camera_preset": {fmt_str(props.camera_preset)},')
    lines.append(f'    "background_type": {fmt_str(props.background_type)},')
    lines.append(f'    "header_preset": {fmt_str(props.header_preset)},')
    lines.append(f'    "preset": {fmt_str(props.preset)},')
    lines.append(f'    "render_preset": {fmt_str(props.render_preset)},')
    lines.append(f'    "outliner_preset": {fmt_str(props.outliner_preset)},')
    lines.append(f'    "text_editor_preset": {fmt_str(props.text_editor_preset)},')
    lines.append(f'    "sun_target_location": {fmt_vec(sun_props.sun_target_location)},')
    lines.append(f'    "sun_rotation": {fmt_vec(sun_props.sun_rotation)},')
    lines.append(f'    "sun_location": {fmt_vec(sun_props.sun_location)},')
    lines.append(f'    "sun_strength": {sun_props.sun_strength:.2f},')
    lines.append(f'    "custom_grid_scale": {props.custom_grid_scale:.2f},')
    lines.append(f'    "grid_color": {fmt_vec(props.grid_color)},')
    lines.append(f'    "wire_color": {fmt_vec(props.wire_color)},')
    lines.append(f'    "camera_color": {fmt_vec(props.camera_color)},')
    lines.append(f'    "header_color": {fmt_vec(props.header_color)},')
    lines.append(f'    "custom_gradient_high": {fmt_vec(props.custom_gradient_high)},')
    lines.append(f'    "custom_gradient_low": {fmt_vec(props.custom_gradient_low)},')
    lines.append(f'    "reverse_gradient": {props.reverse_gradient},')
    lines.append(f'    "render_color": {fmt_vec(props.render_color)},')
    lines.append(f'    "render_environment_strength": {props.render_environment_strength:.2f},')
    lines.append(f'    "outliner_header_color": {fmt_vec(props.outliner_header_color)},')
    lines.append(f'    "outliner_background_color": {fmt_vec(props.outliner_background_color)},')
    lines.append(f'    "text_editor_header_color": {fmt_vec(props.text_editor_header_color)},')
    lines.append(f'    "text_editor_background_color": {fmt_vec(props.text_editor_background_color)},')
    lines.append(f'    "view_pos": {fmt_vec(persp_props.view_pos)},')
    lines.append("}")
    lines.append("# ===" + " DEFAULTS_BLOCK_END ===")
    return chr(10).join(lines)

def generate_base_presets_str(new_preset=None):
    def fmt_vec(v): return "(" + ", ".join(f"{x:.2f}" for x in v) + ")"
    def fmt_str(s): return f'"{s}"'
    
    lines = []
    lines.append("# ===" + " PRESETS_BLOCK_START ===")
    lines.append("BASE_PRESETS =[")
    for p in BASE_PRESETS:
        lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}, {fmt_vec(p[3])}, {fmt_vec(p[4])}),")
    
    if new_preset:
        lines.append(f"    ({fmt_str(new_preset[0])}, {fmt_str(new_preset[1])}, {fmt_str(new_preset[2])}, {fmt_vec(new_preset[3])}, {fmt_vec(new_preset[4])}),")
        
    lines.append("]")
    lines.append("# ===" + " PRESETS_BLOCK_END ===")
    return chr(10).join(lines)

def replace_source_code_blocks(source, new_defaults=None, new_base_presets=None):
    """ 行頭(^)を要求することで、インデントされている内部コードを絶対に巻き込まない """
    if new_defaults:
        pat_def = r"^# === DEFAULTS_BLOCK_START ===.*?^# === DEFAULTS_BLOCK_END ==="
        source = re.sub(pat_def, new_defaults, source, flags=re.DOTALL | re.MULTILINE)
    if new_base_presets:
        pat_pre = r"^# === PRESETS_BLOCK_START ===.*?^# === PRESETS_BLOCK_END ==="
        source = re.sub(pat_pre, new_base_presets, source, flags=re.DOTALL | re.MULTILINE)
    return source

def show_text_in_editor(context, text_obj):
    context.window_manager.clipboard = text_obj.as_string()
    shown = False
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'TEXT_EDITOR':
                area.spaces.active.text = text_obj
                shown = True
    return shown

# ==============================================================================
#  リアルタイム更新用コールバック関数
# ==============================================================================
def update_custom_grid_scale(self, context):
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D': area.spaces.active.overlay.grid_scale = self.custom_grid_scale

def update_grid_color(self, context): bpy.context.preferences.themes[0].view_3d.grid = self.grid_color
def update_wire_color(self, context): bpy.context.preferences.themes[0].view_3d.wire = self.wire_color
def update_camera_color(self, context): bpy.context.preferences.themes[0].view_3d.camera = self.camera_color

def update_background_color(self, context):
    gradients = bpy.context.preferences.themes[0].view_3d.space.gradients
    if self.background_type == 'SINGLE_COLOR':
        gradients.background_type = 'LINEAR'
        gradients.high_gradient = gradients.gradient = self.custom_gradient_low if self.reverse_gradient else self.custom_gradient_high
    else:
        gradients.background_type = self.background_type
        gradients.high_gradient = self.custom_gradient_low if self.reverse_gradient else self.custom_gradient_high
        gradients.gradient = self.custom_gradient_high if self.reverse_gradient else self.custom_gradient_low
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()

def update_header_color(self, context):
    bpy.context.preferences.themes[0].view_3d.space.header = self.header_color
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()

def update_render_color(self, context):
    bpy.context.preferences.themes[0].image_editor.space.back = self.render_color[:3]
    world = bpy.data.worlds.get('MyWorld') or bpy.data.worlds.new('MyWorld')
    context.scene.world = world
    world.use_nodes = True
    bg_node = world.node_tree.nodes.get('Background') or world.node_tree.nodes.new(type='ShaderNodeBackground')
    bg_node.name = 'Background'
    bg_node.inputs[0].default_value = self.render_color
    bg_node.inputs[1].default_value = self.render_environment_strength
    output_node = world.node_tree.nodes.get('World Output') or world.node_tree.nodes.new(type='ShaderNodeOutputWorld')
    output_node.name = 'World Output'
    world.node_tree.links.new(bg_node.outputs[0], output_node.inputs['Surface'])

def update_outliner_color(self, context):
    space = bpy.context.preferences.themes[0].outliner.space
    space.header = self.outliner_header_color
    space.back = self.outliner_background_color[:3]
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'OUTLINER': area.tag_redraw()

def update_text_editor_color(self, context):
    space = bpy.context.preferences.themes[0].text_editor.space
    space.header = self.text_editor_header_color
    space.back = self.text_editor_background_color[:3]
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'TEXT_EDITOR': area.tag_redraw()

def get_or_create_sun():
    sun_obj = bpy.data.objects.get("Sun")
    if sun_obj is None or sun_obj.type != 'LIGHT' or sun_obj.data.type != 'SUN':
        if sun_obj:
            try: bpy.data.objects.remove(sun_obj, do_unlink=True)
            except: pass
        bpy.ops.object.light_add(type='SUN', align='WORLD', location=(0, 0, 0))
        sun_obj = bpy.context.active_object
        sun_obj.name = "Sun"; sun_obj.data.name = "Sun"
    return sun_obj

def update_sun(self, context):
    sun = get_or_create_sun()
    sun.location = self.sun_location
    if self.sun_control_mode == 'ANGLE':
        sun.rotation_euler = self.sun_rotation
    else:
        target_vec = Vector(self.sun_target_location)
        sun_vec = Vector(self.sun_location)
        if (target_vec - sun_vec).length_squared < 0.0001: return
        direction = target_vec - sun_vec
        sun.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()
    sun.data.energy = self.sun_strength

# ==============================================================================
#  リアルタイム更新用コールバック関数(透視投影 視座位置)
# ==============================================================================
_is_updating_view = False

def update_view_position(self, context):
    global _is_updating_view
    if _is_updating_view: return
    props = getattr(context.scene, "persp_view_props", None)
    if not props: return
    limit = props.slider_limit
    v = list(props.view_pos)
    clamped = False
    for i in range(3):
        if v[i] > limit: v[i] = limit; clamped = True
        elif v[i] < -limit: v[i] = -limit; clamped = True
    if clamped:
        _is_updating_view = True
        props.view_pos = v
        _is_updating_view = False
    
    _is_updating_view = True
    try:
        cam_pos = Vector(props.view_pos)
        for window in context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            r3d = space.region_3d
                            r3d.view_perspective = 'PERSP'
                            target_pos = Vector(r3d.view_location)
                            rel_pos = cam_pos - target_pos
                            dist = rel_pos.length
                            if dist > 0.001:
                                r3d.view_distance = dist
                                r3d.view_rotation = rel_pos.to_track_quat('Z', 'Y')
    finally:
        _is_updating_view = False

def view_sync_timer():
    global _is_updating_view
    if _is_updating_view: return 0.05
    context = bpy.context
    if getattr(context, "scene", None) is None: return 0.05
    props = getattr(context.scene, "persp_view_props", None)
    if not props: return 0.05
    r3d, target_area = None, None
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        r3d, target_area = space.region_3d, area
                        break
                if r3d: break
        if r3d: break

    if r3d and target_area:
        target_pos = Vector(r3d.view_location)
        actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        current_pos = Vector(props.view_pos)
        if (current_pos - actual_cam_pos).length > 0.001:
            _is_updating_view = True
            max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
            if max_val > props.slider_limit: props.slider_limit = max_val + 50.0 
            props.view_pos = actual_cam_pos
            _is_updating_view = False
            target_area.tag_redraw()
    return 0.05

# ------------------------------------------------------------------------
# Property Groups
# ------------------------------------------------------------------------
class SunSettingsProperties(PropertyGroup):
    sun_control_mode: EnumProperty(name="制御モード", items=[('ANGLE', "角度", "太陽の回転を直接指定"), ('TARGET', "ターゲット", "指定位置に太陽を向ける")], default=CURRENT_DEFAULTS.get("sun_control_mode", 'ANGLE'), update=update_sun)
    sun_target_location: FloatVectorProperty(name="ターゲット位置", subtype='XYZ', default=CURRENT_DEFAULTS.get("sun_target_location", (0.0,0.0,0.0)), update=update_sun)
    sun_rotation: FloatVectorProperty(name="角度", subtype='EULER', unit='ROTATION', default=CURRENT_DEFAULTS.get("sun_rotation", (0.79,0,0.79)), update=update_sun)
    sun_location: FloatVectorProperty(name="位置", subtype='XYZ', default=CURRENT_DEFAULTS.get("sun_location", (0.0,0.0,10.0)), update=update_sun)
    sun_strength: FloatProperty(name="強さ", default=CURRENT_DEFAULTS.get("sun_strength", 2.5), min=0.0, update=update_sun)

class ViewportColorProperties(PropertyGroup):
    custom_grid_scale: FloatProperty(name="Scale", default=CURRENT_DEFAULTS.get("custom_grid_scale", 1.0), min=0.001, update=update_custom_grid_scale)
    grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("grid_color", (0.2,0.2,0.2,1.0)), update=update_grid_color)
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], default=get_safe_default(CURRENT_DEFAULTS.get("grid_preset", ""), GRID_PRESETS), update=lambda self, context: self.update_grid_preset(context))
    wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("wire_color", (0,0,0)), update=update_wire_color)
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], default=get_safe_default(CURRENT_DEFAULTS.get("wire_preset", ""), WIRE_PRESETS), update=lambda self, context: self.update_wire_preset(context))
    camera_color: FloatVectorProperty(name="Camera Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("camera_color", (0.47,0.55,1.0)), update=update_camera_color)
    camera_preset: EnumProperty(name="Camera Preset", items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS], default=get_safe_default(CURRENT_DEFAULTS.get("camera_preset", ""), CAMERA_PRESETS), update=lambda self, context: self.update_camera_preset(context))
    background_type: EnumProperty(name="Background Type", items=BACKGROUND_TYPES, default=CURRENT_DEFAULTS.get("background_type", 'LINEAR'), update=update_background_color)
    header_color: FloatVectorProperty(name="Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("header_color", (0.0,0.03,0.0,1.0)), update=update_header_color)
    header_preset: EnumProperty(name="Header Preset", items=[(p[0], p[1], p[2]) for p in HEADER_PRESETS], default=get_safe_default(CURRENT_DEFAULTS.get("header_preset", ""), HEADER_PRESETS), update=lambda self, context: self.update_header_preset(context))
    custom_gradient_high: FloatVectorProperty(name="Gradient High Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("custom_gradient_high", (0.16,0.77,1.0)), update=update_background_color)
    custom_gradient_low: FloatVectorProperty(name="Gradient Low Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("custom_gradient_low", (0.01,0.01,0.15)), update=update_background_color)
    reverse_gradient: BoolProperty(name="Reverse Gradient", default=CURRENT_DEFAULTS.get("reverse_gradient", False), update=update_background_color)
    preset: EnumProperty(name="Color Preset", items=[(p[0], p[1], p[2]) for p in BASE_PRESETS], default=get_safe_default(CURRENT_DEFAULTS.get("preset", ""), BASE_PRESETS), update=lambda self, context: self.update_preset(context))
    render_color: FloatVectorProperty(name="Render Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("render_color", (0.19,0.6,1.0,1.0)), update=update_render_color)
    render_preset: EnumProperty(name="Render Preset", items=[(p[0], p[1], p[2]) for p in RENDER_PRESETS], default=get_safe_default(CURRENT_DEFAULTS.get("render_preset", ""), RENDER_PRESETS), update=lambda self, context: self.update_render_preset(context))
    render_environment_strength: FloatProperty(name="Render Environment Strength", default=CURRENT_DEFAULTS.get("render_environment_strength", 1.0), min=0.0, max=1900.0, update=update_render_color)
    outliner_header_color: FloatVectorProperty(name="Outliner Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("outliner_header_color", (0.19,0.19,0.19,0.7)), update=update_outliner_color)
    outliner_background_color: FloatVectorProperty(name="Outliner Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("outliner_background_color", (0.14,0.14,0.14,1.0)), update=update_outliner_color)
    outliner_preset: EnumProperty(name="Outliner Preset", items=[(p[0], p[1], p[2]) for p in OUTLINER_PRESETS], default=get_safe_default(CURRENT_DEFAULTS.get("outliner_preset", ""), OUTLINER_PRESETS), update=lambda self, context: self.update_outliner_preset(context))
    text_editor_header_color: FloatVectorProperty(name="Text Editor Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("text_editor_header_color", (0.19,0.19,0.19,0.7)), update=update_text_editor_color)
    text_editor_background_color: FloatVectorProperty(name="Text Editor Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS.get("text_editor_background_color", (0.14,0.14,0.14,1.0)), update=update_text_editor_color)
    text_editor_preset: EnumProperty(name="Text Editor Preset", items=[(p[0], p[1], p[2]) for p in TEXT_EDITOR_PRESETS], default=get_safe_default(CURRENT_DEFAULTS.get("text_editor_preset", ""), TEXT_EDITOR_PRESETS), update=lambda self, context: self.update_text_editor_preset(context))

    def update_grid_preset(self, context):
        for p in GRID_PRESETS:
            if p[0] == self.grid_preset and len(p) >= 4: self.grid_color = p[3]; break
    def update_wire_preset(self, context):
        for p in WIRE_PRESETS:
            if p[0] == self.wire_preset and len(p) >= 4: self.wire_color = p[3]; break
    def update_camera_preset(self, context):
        for p in CAMERA_PRESETS:
            if p[0] == self.camera_preset and len(p) >= 4: self.camera_color = p[3]; break
    def update_preset(self, context):
        for p in BASE_PRESETS:
            if p[0] == self.preset and len(p) >= 5: self.custom_gradient_high = p[3]; self.custom_gradient_low = p[4]; break
    def update_header_preset(self, context):
        for p in HEADER_PRESETS:
            if p[0] == self.header_preset and len(p) >= 4: self.header_color = p[3]; break
    def update_render_preset(self, context):
        for p in RENDER_PRESETS:
            if p[0] == self.render_preset and len(p) >= 4: self.render_color = p[3]; break
    def update_outliner_preset(self, context):
        for p in OUTLINER_PRESETS:
            if p[0] == self.outliner_preset and len(p) >= 5: self.outliner_header_color = p[3]; self.outliner_background_color = p[4]; break
    def update_text_editor_preset(self, context):
        for p in TEXT_EDITOR_PRESETS:
            if p[0] == self.text_editor_preset and len(p) >= 5: self.text_editor_header_color = p[3]; self.text_editor_background_color = p[4]; break

class PerspViewProperties(PropertyGroup):
    slider_limit: FloatProperty(name="Range Limit", default=300.0, min=10.0, max=10000.0)
    view_pos: FloatVectorProperty(name="View Position", size=3, soft_min=-10000.0, soft_max=10000.0, default=CURRENT_DEFAULTS.get('view_pos', VIEW_POS_INIT), update=update_view_position)

# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OT_ViewCenterFront(Operator):
    bl_idname = f"{PREFIX}_wm.view_center_front"
    bl_label = "Center 0,0,0 (Front View)"
    bl_description = "原点(0,0,0)を画面中央に配置し、Yマイナス方向からの視点(正面)にします"
    def execute(self, context):
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                rv3d = area.spaces.active.region_3d
                if rv3d:
                    rv3d.view_location = (0.0, 0.0, 0.0)
                    rv3d.view_rotation = Euler((radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
                    if rv3d.view_distance < 10.0: rv3d.view_distance = 60.0
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{PREFIX}_wm.copy_script"
    bl_label = "Copy Defaults Only"
    def execute(self, context):
        new_dict = generate_current_defaults_str(context)
        context.window_manager.clipboard = new_dict
        self.report({'INFO'}, "最新数値を初期値コードとしてクリップボードにコピーしました!")
        return {'FINISHED'}

# ==============================================================================
# アドオン全体書き出しオペレーター群
# ==============================================================================
class OT_ExportAddonWithCurrentDefaults(Operator):
    bl_idname = f"{PREFIX}_wm.export_addon_current_defaults"
    bl_label = "現在の設定を初期値にしてアドオン全体を書き出し"
    
    def execute(self, context):
        src = get_own_source_code()
        if not src:
            self.report({'ERROR'}, "元のアドオンのソースコードを取得できませんでした。")
            return {'CANCELLED'}
            
        new_defaults = generate_current_defaults_str(context)
        new_src = replace_source_code_blocks(src, new_defaults=new_defaults)
        
        text_name = "Exported_Addon.py"
        text = bpy.data.texts.get(text_name) or bpy.data.texts.new(text_name)
        text.clear()
        text.write(new_src)
        
        if show_text_in_editor(context, text):
            self.report({'INFO'}, f"エディタに '{text_name}' を表示しました!(クリップボードにもコピー済)")
        else:
            self.report({'INFO'}, f"'{text_name}' を内部作成しました。テキストエディタ画面を開いて選択してください (クリップボードにもコピー済)")
            
        return {'FINISHED'}

class OT_ExportAddonWithNewPreset(Operator):
    bl_idname = f"{PREFIX}_wm.export_addon_new_preset"
    bl_label = "現在の背景色をプリセットに追加してアドオン全体を書き出し"
    
    preset_id: StringProperty(name="ID (英数字)", default="NEW_COLOR")
    preset_name: StringProperty(name="表示名", default="New Color")
    preset_desc: StringProperty(name="説明", default="New custom background")
    
    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)
        
    def execute(self, context):
        src = get_own_source_code()
        if not src:
            self.report({'ERROR'}, "元のアドオンのソースコードを取得できませんでした。")
            return {'CANCELLED'}
            
        # プリセット追加の文字列表現を作成
        props = context.scene.viewport_color_props
        high = tuple(props.custom_gradient_high[:3])
        low = tuple(props.custom_gradient_low[:3])
        new_preset = (self.preset_id, self.preset_name, self.preset_desc, high, low)
        new_presets_str = generate_base_presets_str(new_preset=new_preset)
        
        # デフォルト値も現在の値にする(今作ったプリセットを初期値にする)
        new_defaults = generate_current_defaults_str(context)
        new_defaults = re.sub(r'"preset": \'.*?\'', f'"preset": \'{self.preset_id}\'', new_defaults)

        new_src = replace_source_code_blocks(src, new_defaults=new_defaults, new_base_presets=new_presets_str)
        
        text_name = "Exported_Addon_NewPreset.py"
        text = bpy.data.texts.get(text_name) or bpy.data.texts.new(text_name)
        text.clear()
        text.write(new_src)
        
        if show_text_in_editor(context, text):
            self.report({'INFO'}, f"エディタに '{text_name}' を表示しました!(クリップボードにもコピー済)")
        else:
            self.report({'INFO'}, f"'{text_name}' を内部作成しました。テキストエディタ画面を開いて選択してください (クリップボードにもコピー済)")
            
        return {'FINISHED'}

# ==============================================================================
# (旧)部分書き出しオペレーター
# ==============================================================================
class OT_ExportAllPresets(Operator):
    bl_idname = f"{PREFIX}_wm.export_all_presets"
    bl_label = "全プリセット定義をテキスト出力"
    
    def execute(self, context):
        text_name = "Exported_Presets.py"
        text = bpy.data.texts.get(text_name) or bpy.data.texts.new(text_name)
        text.clear()
        
        def fmt_vec(v): return "(" + ", ".join(f"{x:.2f}" for x in v) + ")"
        def fmt_str(s): return f"'{s}'" if s in ['SINGLE_COLOR', 'LINEAR', 'RADIAL'] else f'"{s}"'
        
        lines = []
        lines.append("# プリセット群")
        
        lines.append("BASE_PRESETS =[")
        for p in BASE_PRESETS: lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}, {fmt_vec(p[3])}, {fmt_vec(p[4])}),")
        lines.append("]")
        
        lines.append("HEADER_PRESETS =[")
        for p in HEADER_PRESETS: lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}, {fmt_vec(p[3])}),")
        lines.append("]")
        
        lines.append("RENDER_PRESETS =[")
        for p in RENDER_PRESETS: lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}, {fmt_vec(p[3])}),")
        lines.append("]")
        
        lines.append("GRID_PRESETS =[")
        for p in GRID_PRESETS: lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}, {fmt_vec(p[3])}),")
        lines.append("]")
        
        lines.append("WIRE_PRESETS =[")
        for p in WIRE_PRESETS: lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}, {fmt_vec(p[3])}),")
        lines.append("]")
        
        lines.append("CAMERA_PRESETS =[")
        for p in CAMERA_PRESETS: lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}, {fmt_vec(p[3])}),")
        lines.append("]")
        
        lines.append("OUTLINER_PRESETS =[")
        for p in OUTLINER_PRESETS: lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}, {fmt_vec(p[3])}, {fmt_vec(p[4])}),")
        lines.append("]")
        
        lines.append("TEXT_EDITOR_PRESETS =[")
        for p in TEXT_EDITOR_PRESETS: lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}, {fmt_vec(p[3])}, {fmt_vec(p[4])}),")
        lines.append("]")
        
        lines.append("BACKGROUND_TYPES =[")
        for p in BACKGROUND_TYPES: lines.append(f"    ({fmt_str(p[0])}, {fmt_str(p[1])}, {fmt_str(p[2])}),")
        lines.append("]")

        full_text = chr(10).join(lines) + chr(10)
        text.write(full_text)
        
        if show_text_in_editor(context, text):
            self.report({'INFO'}, f"エディタに '{text_name}' を表示しました! (クリップボードにもコピー済)")
        else:
            self.report({'INFO'}, f"'{text_name}' を内部作成しました。テキストエディタ画面を開いて選択してください (クリップボードにもコピー済)")

        return {'FINISHED'}

class OT_ExportCurrentBgToPresetFormat(Operator):
    bl_idname = f"{PREFIX}_wm.export_current_bg"
    bl_label = "現在の背景色をプリセット形式で出力"
    
    preset_id: StringProperty(name="ID (英数字)", default="NEW_COLOR")
    preset_name: StringProperty(name="表示名", default="New Color")
    preset_desc: StringProperty(name="説明", default="New custom background")
    
    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)
        
    def execute(self, context):
        props = context.scene.viewport_color_props
        def fmt_vec(v): return "(" + ", ".join(f"{x:.2f}" for x in v) + ")"
        
        high = fmt_vec(props.custom_gradient_high[:3])
        low = fmt_vec(props.custom_gradient_low[:3])
        
        line = f'    ("{self.preset_id}", "{self.preset_name}", "{self.preset_desc}", {high}, {low}),'
        
        text_name = "New_Background_Preset.py"
        text = bpy.data.texts.get(text_name) or bpy.data.texts.new(text_name)
        text.clear()
        
        output = "# 以下の行をスクリプト内の BASE_PRESETS リストに追加してください。" + chr(10) + line + chr(10)
        text.write(output)
        
        if show_text_in_editor(context, text):
            self.report({'INFO'}, f"エディタに '{text_name}' を表示しました! (クリップボードにもコピー済)")
        else:
            self.report({'INFO'}, f"'{text_name}' を内部作成しました。テキストエディタ画面を開いて選択してください (クリップボードにもコピー済)")
            
        return {'FINISHED'}

class OVERLAY_OT_set_grid_scale(Operator):
    bl_idname = f"{PREFIX}_overlay.set_grid_scale"; bl_label = "Set Grid Scale"
    scale_value: FloatProperty()
    def execute(self, context):
        for window in context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D': area.spaces.active.overlay.grid_scale = self.scale_value
        return {'FINISHED'}

class SUN_OT_Create(Operator):
    bl_idname = f"{PREFIX}.create_sun"; bl_label = "太陽を作成"
    def execute(self, context):
        get_or_create_sun(); self.report({'INFO'}, "太陽を作成しました。"); return {'FINISHED'}

class SUN_OT_Reset(Operator):
    bl_idname = f"{PREFIX}.reset_sun"; bl_label = "太陽の設定を初期値にリセット"
    def execute(self, context):
        props = context.scene.sun_settings_props
        props.sun_control_mode, props.sun_target_location = 'ANGLE', (0.0, 0.0, 0.0)
        props.sun_rotation = (radians(45.0), radians(0.0), radians(45.0))
        props.sun_location, props.sun_strength = (0.0, 0.0, 10.0), 2.5
        update_sun(props, context)
        self.report({'INFO'}, "太陽の設定をリセットしました。"); return {'FINISHED'}

class RemoveAllPanels(Operator):
    bl_idname = f"{PREFIX}_wm.remove_all_panels"; bl_label = PANEL_LABELS["REMOVE"]
    def execute(self, context): unregister(); return {'FINISHED'}

class PERSP_OT_GetCurrentView(Operator):
    bl_idname = f"{PREFIX}_persp.get_current_view"; bl_label = "Get Current View & Update"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, "persp_view_props", None)
        r3d = context.space_data.region_3d if context.space_data else None
        if not props or not r3d: return {'CANCELLED'}
        target_pos = Vector(r3d.view_location)
        actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
        if max_val > props.slider_limit: props.slider_limit = max_val + 50.0 
        props.view_pos = actual_cam_pos; return {'FINISHED'}

class PERSP_OT_ResetView(Operator):
    bl_idname = f"{PREFIX}_persp.reset_view"; bl_label = VIEW_RESET_BTN_TEXT; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, "persp_view_props", None)
        if props:
            for window in context.window_manager.windows:
                for area in window.screen.areas:
                    if area.type == 'VIEW_3D':
                        for space in area.spaces:
                            if space.type == 'VIEW_3D': space.region_3d.view_location = (0.0, 0.0, 0.0)
            props.view_pos = VIEW_POS_INIT 
        return {'FINISHED'}

class PERSP_OT_CenterSelected(Operator):
    bl_idname = f"{PREFIX}_persp.center_selected"; bl_label = "Center Selected Object"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        bpy.ops.view3d.view_selected()
        r3d = context.space_data.region_3d if context.space_data else None
        props = getattr(context.scene, "persp_view_props", None)
        if r3d and props:
            target_pos = Vector(r3d.view_location)
            props.view_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        return {'FINISHED'}

class PERSP_OT_CopyActualViewPos(Operator):
    bl_idname = f"{PREFIX}_persp.copy_actual_pos"; bl_label = "Copy Position Only"
    def execute(self, context):
        r3d = context.space_data.region_3d if context.space_data else None
        if not r3d: return {'CANCELLED'}
        target_pos = Vector(r3d.view_location)
        p = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        context.window_manager.clipboard = f"Actual View Pos: ({p.x:.2f}, {p.y:.2f}, {p.z:.2f})"
        self.report({'INFO'}, "視座位置をコピーしました"); return {'FINISHED'}

class PERSP_OT_CopyAngles(Operator):
    bl_idname = f"{PREFIX}_persp.copy_angles"; bl_label = "情報パネル 一括コピー"
    def execute(self, context):
        props = getattr(context.scene, "persp_view_props", None)
        r3d = context.space_data.region_3d if context.space_data else None
        if not r3d or not props: return {'CANCELLED'}
        target_pos = Vector(r3d.view_location)
        actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        vec = target_pos - actual_cam_pos; length = vec.length
        if length < 0.0001: return {'CANCELLED'}
        ang_x, ang_y, ang_z = math.degrees(math.acos(vec.x / length)), math.degrees(math.acos(vec.y / length)), math.degrees(math.acos(vec.z / length))
        pl_x, pl_y, pl_z = math.degrees(math.asin(vec.x / length)), math.degrees(math.asin(vec.y / length)), math.degrees(math.asin(vec.z / length))
        
        mode_str = "透視投影" if r3d.view_perspective == 'PERSP' else "平行投影" if r3d.view_perspective == 'ORTHO' else "カメラ"
        t_x, t_y, t_z = target_pos.x, target_pos.y, target_pos.z
        c_x, c_y, c_z = actual_cam_pos.x, actual_cam_pos.y, actual_cam_pos.z
        dist_to_00 = actual_cam_pos.length
        
        lens = context.space_data.lens if hasattr(context.space_data, 'lens') else 50.0
        width_size = r3d.view_distance * (32.0 / lens)
        aspect = context.area.height / context.area.width if context.area and context.area.width > 0 else 1.0
        height_size = width_size * aspect
        
        info_lines = [
            f"投影モード: {mode_str}",
            f"画面中央位置: X: {t_x:.1f}, Y: {t_y:.1f}, Z: {t_z:.1f}",
            f"視座位置: X: {c_x:.1f}, Y: {c_y:.1f}, Z: {c_z:.1f}",
        ]
        if r3d.view_perspective == 'PERSP':
            info_lines.append(f"視座からの00への距離: {dist_to_00:.1f}")
            
        area_w = context.area.width if context.area else 0
        area_h = context.area.height if context.area else 0

        info_lines.extend([
            f"画面水平の大きさ: {width_size:.1f}",
            f"画面上下の大きさ: {height_size:.1f}",
            "",
            "[ Viewport Size ]",
            f"Width : {area_w} px",
            f"Height: {area_h} px",
            "",
            "--- View Direction Info ---",
            "[ Actual 3D View Status ]",
            f"Actual View Pos : ({c_x:.2f}, {c_y:.2f}, {c_z:.2f})",
            f"Target Pos      : ({t_x:.2f}, {t_y:.2f}, {t_z:.2f})",
            f"Distance        : {length:.2f}",
            "",
            "[ Direction Angles (軸そのものとの角度 0〜180°) ]",
            f"Angle from X Axis : {ang_x:.2f} deg",
            f"Angle from Y Axis : {ang_y:.2f} deg",
            f"Angle from Z Axis : {ang_z:.2f} deg",
            "",
            "[ Planar Angles (直感的な傾き・ズレ角 -90〜90°) ]",
            f"X (横のズレ角)    : {pl_x:.2f} deg",
            f"Y (前後の傾き)    : {pl_y:.2f} deg",
            f"Z (仰角・俯角)    : {pl_z:.2f} deg",
        ])
        
        context.window_manager.clipboard = chr(10).join(info_lines)
        self.report({'INFO'}, "情報パネルを一括コピーしました"); return {'FINISHED'}

# ------------------------------------------------------------------------
# Panels
# ------------------------------------------------------------------------
class BasePanel(Panel): bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME

class VIEW3D_PT_solid_background_panel(BasePanel):
    bl_label = PANEL_LABELS["BACKGROUND"]; bl_idname = BG_PANEL_IDNAME_1; bl_order = 0
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        layout.prop(props, "preset", text="Background Preset"); layout.prop(props, "background_type", expand=True)
        layout.prop(props, "custom_gradient_high", text="Color High" if props.background_type != 'SINGLE_COLOR' else "Color")
        if props.background_type != 'SINGLE_COLOR': layout.prop(props, "custom_gradient_low")
        layout.prop(props, "reverse_gradient", text="Reverse Gradient")

class VIEW3D_PT_GridPanel(BasePanel):
    bl_label = PANEL_LABELS["GRID"]; bl_idname = GRID_PANEL_IDNAME; bl_order = 1
    def draw(self, context): layout, props = self.layout, context.scene.viewport_color_props; layout.prop(props, "grid_preset"); layout.prop(props, "grid_color")

class VIEW3D_PT_MainPanel(BasePanel):
    bl_label = PANEL_LABELS["MAIN"]; bl_idname = MAIN_PANEL_IDNAME; bl_order = 2
    def draw(self, context):
        layout = self.layout
        
        # メイン操作群
        layout.operator(f"{PREFIX}_persp.center_selected", icon='VIEWZOOM', text="選択Objectを画面中央に表示")
        layout.separator()
        row = layout.row(); row.scale_y = 1.2
        row.operator(f"{PREFIX}_wm.copy_script", icon='COPY_ID', text="初期値コード(辞書)のみをコピー")
        layout.row().operator(f"{PREFIX}_wm.view_center_front", icon='VIEWZOOM', text="0,0,0 を正面(Y-)から見る")
        
        layout.separator()
        
        # Overlays設定群
        layout.label(text="Overlays", icon='OVERLAY')
        props = context.scene.viewport_color_props
        if context.space_data.type == 'VIEW_3D':
            layout.prop(context.space_data.overlay, "show_floor", text="Floor")
            layout.prop(props, "custom_grid_scale", text="Scale 数値入力")
            row_grid = layout.row(align=True)
            row_grid.operator(f"{PREFIX}_overlay.set_grid_scale", text="入力値").scale_value = props.custom_grid_scale
            row_grid.operator(f"{PREFIX}_overlay.set_grid_scale", text="10.0").scale_value = 10.0
            row_grid.operator(f"{PREFIX}_overlay.set_grid_scale", text="100.0").scale_value = 100.0
        else:
            layout.label(text="3D Viewport is required.")

        layout.separator()
        
        # --- 新規追加:アドオン全体書き出し操作群 ---
        layout.label(text="アドオン書き出し (完全なPythonコード)", icon='SCRIPT')
        layout.operator(f"{PREFIX}_wm.export_addon_current_defaults", icon='FILE_SCRIPT', text="現在の設定を初期値にして アドオン全体書き出し")
        layout.operator(f"{PREFIX}_wm.export_addon_new_preset", icon='PLUS', text="現在の背景色をプリセットに追加して アドオン全体書き出し")
        
        layout.separator()
        
        # --- 旧:部分的なコードの書き出し ---
        layout.label(text="エディタ / クリップボード出力 (コードの一部のみ)", icon='TEXT')
        layout.operator(f"{PREFIX}_wm.export_all_presets", icon='FILE_TEXT', text="全プリセットを書き出し")
        layout.operator(f"{PREFIX}_wm.export_current_bg", icon='PLUS', text="現在の背景色をプリセット形式で出力")

class VIEW3D_PT_PerspControlPanel(BasePanel):
    bl_label = PANEL_LABELS["PERSP"]; bl_idname = PERSP_PANEL_IDNAME; bl_order = 3
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, "persp_view_props", None)
        if not props: return
        box = layout.box(); box.label(text="Perspective Viewpoint", icon='VIEW_CAMERA'); box.prop(props, "slider_limit", text="Range Limit (+/-)")
        col = box.column(align=True); col.prop(props, "view_pos", text="X", index=0); col.prop(props, "view_pos", text="Y", index=1); col.prop(props, "view_pos", text="Z", index=2)
        box.separator(); box.operator(f"{PREFIX}_persp.get_current_view", icon='RESTRICT_VIEW_OFF'); box.operator(f"{PREFIX}_persp.reset_view", icon='LOOP_BACK')
        layout.separator()
        
        box_info = layout.box(); box_info.label(text="Actual View Status", icon='INFO')
        
        # 画面大きさ情報
        if context.area:
            col_size = box_info.column(align=True)
            col_size.label(text="[ Viewport Size ]", icon='WINDOW')
            col_size.label(text=f"  Width : {context.area.width} px")
            col_size.label(text=f"  Height: {context.area.height} px")
            box_info.separator()
            
        r3d = context.space_data.region_3d if context.space_data else None
        if r3d:
            target_pos = Vector(r3d.view_location); actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
            vec = target_pos - actual_cam_pos; length = vec.length
            col_pos = box_info.column(align=True); col_pos.label(text="[ Actual Position ]", icon='VIEW_CAMERA')
            col_pos.label(text=f"  X: {actual_cam_pos.x:.2f}"); col_pos.label(text=f"  Y: {actual_cam_pos.y:.2f}"); col_pos.label(text=f"  Z: {actual_cam_pos.z:.2f}"); col_pos.label(text=f"  Distance: {length:.2f}") 
            box_info.operator(f"{PREFIX}_persp.copy_actual_pos", icon='COPYDOWN'); box_info.separator()
            col_ang = box_info.column(align=True)
            if length > 0.0001:
                a_x, a_y, a_z = math.degrees(math.acos(vec.x / length)), math.degrees(math.acos(vec.y / length)), math.degrees(math.acos(vec.z / length))
                p_x, p_y, p_z = math.degrees(math.asin(vec.x / length)), math.degrees(math.asin(vec.y / length)), math.degrees(math.asin(vec.z / length))
                col_ang.label(text="[ Direction Angles (軸との角度) ]", icon='ORIENTATION_GLOBAL')
                col_ang.label(text=f"  X: {a_x:.2f}°"); col_ang.label(text=f"  Y: {a_y:.2f}°"); col_ang.label(text=f"  Z: {a_z:.2f}°")
                col_ang.separator(); col_ang.label(text="[ Planar Angles (直感的な傾き) ]", icon='DRIVER_ROTATIONAL_DIFFERENCE')
                col_ang.label(text=f"  X (ズレ角): {p_x:.2f}°"); col_ang.label(text=f"  Y (ズレ角): {p_y:.2f}°"); col_ang.label(text=f"  Z (仰俯角): {p_z:.2f}°")
            else: col_ang.label(text="  Target is too close")
            box_info.separator(); box_info.operator(f"{PREFIX}_persp.copy_angles", text="情報パネル 一括コピー", icon='COPYDOWN')
        else: box_info.label(text="Please use in 3D View")

class VIEW3D_PT_HeaderPanel(BasePanel):
    bl_label = PANEL_LABELS["HEADER"]; bl_idname = HEADER_PANEL_IDNAME; bl_order = 4
    def draw(self, context): layout, props = self.layout, context.scene.viewport_color_props; layout.prop(props, "header_preset", text="Header Preset"); layout.prop(props, "header_color")

class VIEW3D_PT_RenderPanel(BasePanel):
    bl_label = PANEL_LABELS["RENDER"]; bl_idname = RENDER_PANEL_IDNAME; bl_order = 5
    def draw(self, context): layout, props = self.layout, context.scene.viewport_color_props; layout.prop(props, "render_preset"); layout.prop(props, "render_color"); layout.prop(props, "render_environment_strength")

class VIEW3D_PT_SunPanel(BasePanel):
    bl_label = PANEL_LABELS["SUN"]; bl_idname = SUN_PANEL_IDNAME; bl_order = 6
    def draw(self, context):
        layout, props = self.layout, context.scene.sun_settings_props
        row = layout.row(align=True); row.operator(f"{PREFIX}.create_sun", text="太陽作成ボタン"); row.operator(f"{PREFIX}.reset_sun", icon='FILE_REFRESH', text="")
        layout.separator(); layout.prop(props, "sun_control_mode", expand=True)
        if props.sun_control_mode == 'ANGLE': layout.prop(props, "sun_rotation")
        else: layout.prop(props, "sun_target_location")
        layout.prop(props, "sun_location"); layout.prop(props, "sun_strength")

class VIEW3D_PT_WirePanel(BasePanel):
    bl_label = PANEL_LABELS["WIRE"]; bl_idname = WIRE_PANEL_IDNAME; bl_order = 7
    def draw(self, context): layout, props = self.layout, context.scene.viewport_color_props; layout.prop(props, "wire_preset"); layout.prop(props, "wire_color")

class VIEW3D_PT_CameraPanel(BasePanel):
    bl_label = PANEL_LABELS["CAMERA"]; bl_idname = CAMERA_PANEL_IDNAME; bl_order = 8
    def draw(self, context): layout, props = self.layout, context.scene.viewport_color_props; layout.prop(props, "camera_preset"); layout.prop(props, "camera_color")

class VIEW3D_PT_OutlinerPanel(BasePanel):
    bl_label = PANEL_LABELS["OUTLINER"]; bl_idname = OUTLINER_PANEL_IDNAME; bl_order = 9
    def draw(self, context): layout, props = self.layout, context.scene.viewport_color_props; layout.prop(props, "outliner_preset"); layout.prop(props, "outliner_header_color"); layout.prop(props, "outliner_background_color")

class VIEW3D_PT_TextEditorPanel(BasePanel):
    bl_label = PANEL_LABELS["TEXT_EDITOR"]; bl_idname = TEXT_EDITOR_PANEL_IDNAME; bl_order = 10
    def draw(self, context): layout, props = self.layout, context.scene.viewport_color_props; layout.prop(props, "text_editor_preset"); layout.prop(props, "text_editor_header_color"); layout.prop(props, "text_editor_background_color")

class VIEW3D_PT_LinkPanel(BasePanel):
    bl_label = PANEL_LABELS["LINK"]; bl_idname = LINK_PANEL_IDNAME; bl_order = 11
    def draw(self, context):
        layout = self.layout
        layout.operator("wm.url_open", text="背景色  20250720", icon='URL').url = "<https://app.notion.com/p/20250720-3a3347b3454a80f78e8dc0a5cc52e649>"
        layout.operator("wm.url_open", text="アドオン コンテナ 20260715", icon='URL').url = "<https://note.com/zionadmillion/n/n2158226ce616>"
        layout.operator("wm.url_open", text="posfie", icon='URL').url = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
        layout.operator("wm.url_open", text="zionadchat", icon='URL').url = "<https://x.com/zionadchat>"

class VIEW3D_PT_RemovePanel(BasePanel):
    bl_label = PANEL_LABELS["REMOVE"]; bl_idname = REMOVE_PANEL_IDNAME; bl_order = 12
    def draw(self, context): self.layout.operator(f"{PREFIX}_wm.remove_all_panels", text=PANEL_LABELS["REMOVE"])

# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes =[
    SunSettingsProperties, ViewportColorProperties, PerspViewProperties,
    OT_CopyFullScript, 
    OT_ExportAddonWithCurrentDefaults, OT_ExportAddonWithNewPreset,
    OT_ExportAllPresets, OT_ExportCurrentBgToPresetFormat, 
    OT_ViewCenterFront, OVERLAY_OT_set_grid_scale,
    SUN_OT_Create, SUN_OT_Reset,
    PERSP_OT_GetCurrentView, PERSP_OT_ResetView, PERSP_OT_CenterSelected, PERSP_OT_CopyActualViewPos, PERSP_OT_CopyAngles,
    RemoveAllPanels, VIEW3D_PT_MainPanel, VIEW3D_PT_PerspControlPanel, VIEW3D_PT_solid_background_panel, 
    VIEW3D_PT_HeaderPanel, VIEW3D_PT_RenderPanel, VIEW3D_PT_SunPanel, VIEW3D_PT_GridPanel, VIEW3D_PT_WirePanel, 
    VIEW3D_PT_CameraPanel, VIEW3D_PT_OutlinerPanel, VIEW3D_PT_TextEditorPanel, VIEW3D_PT_LinkPanel, VIEW3D_PT_RemovePanel
]

def register():
    for cls in classes:
        # 二重登録防止処理
        try: bpy.utils.register_class(cls)
        except ValueError:
            try: bpy.utils.unregister_class(getattr(bpy.types, cls.__name__)); bpy.utils.register_class(cls)
            except Exception: pass
            
    bpy.types.Scene.viewport_color_props = PointerProperty(type=ViewportColorProperties)
    bpy.types.Scene.sun_settings_props = PointerProperty(type=SunSettingsProperties)
    bpy.types.Scene.persp_view_props = PointerProperty(type=PerspViewProperties)
    
    def apply_initial_settings():
        # ウィンドウがまだ構築されていない場合は 0.2 秒後にリトライ(より確実に発動させるための強化措置)
        if not bpy.context.window_manager.windows:
            return 0.2
            
        try:
            if bpy.context.scene and hasattr(bpy.context.scene, 'viewport_color_props'):
                props = bpy.context.scene.viewport_color_props
                sun_props = bpy.context.scene.sun_settings_props
                persp_props = bpy.context.scene.persp_view_props
                
                for key, val in CURRENT_DEFAULTS.items():
                    if hasattr(props, key): setattr(props, key, val)
                    elif hasattr(sun_props, key): setattr(sun_props, key, val)
                    elif hasattr(persp_props, key): setattr(persp_props, key, val)
                
                update_background_color(props, bpy.context); update_header_color(props, bpy.context); update_render_color(props, bpy.context)
                update_grid_color(props, bpy.context); update_wire_color(props, bpy.context); update_camera_color(props, bpy.context)
                update_outliner_color(props, bpy.context); update_text_editor_color(props, bpy.context); update_sun(sun_props, bpy.context)

                # アドオン有効化時の自動設定(サイドパネル展開&マテリアルプレビュー化)
                for window in bpy.context.window_manager.windows:
                    for area in window.screen.areas:
                        if area.type == 'VIEW_3D':
                            for space in area.spaces:
                                if space.type == 'VIEW_3D':
                                    space.show_region_ui = True
                                    space.shading.type = 'MATERIAL'
        except Exception:
            pass
            
        return None # 処理完了でタイマー終了

    bpy.app.timers.register(apply_initial_settings, first_interval=0.2)
    if not bpy.app.timers.is_registered(view_sync_timer): bpy.app.timers.register(view_sync_timer)

def unregister():
    if hasattr(bpy.types.Scene, 'viewport_color_props'): del bpy.types.Scene.viewport_color_props
    if hasattr(bpy.types.Scene, 'sun_settings_props'): del bpy.types.Scene.sun_settings_props
    if hasattr(bpy.types.Scene, 'persp_view_props'): del bpy.types.Scene.persp_view_props
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except RuntimeError: pass
    if bpy.app.timers.is_registered(view_sync_timer): bpy.app.timers.unregister(view_sync_timer)

# ファイルとして直接実行された場合
if __name__ == "__main__":
    if "unregister" in globals():
        try: unregister()
        except Exception: pass
    if "register" in globals(): register()